The Graphite module (src/graphite.rs) provides a Graphite TSDB-compatible API interface, enabling integration with Grafana and other Graphite-compatible tools.
This module implements:
- Graphite render API for time series data
- Metrics discovery API (
/metrics/find) - Grafana-compatible endpoints
Response structure from Graphite queries:
#[derive(Deserialize, Serialize, Debug)]
pub struct GraphiteData {
/// Metric target name
pub target: String,
/// Array of (value, timestamp) tuples
pub datapoints: Vec<(Option<f32>, u32)>,
}Query parameters for metrics discovery:
#[derive(Debug, Deserialize)]
pub struct MetricsQuery {
/// Query pattern (e.g., "flag.*", "health.env1.*")
pub query: String,
/// Optional start time
pub from: Option<String>,
/// Optional end time
pub until: Option<String>,
}Metric metadata for discovery responses:
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Metric {
#[serde(rename(serialize = "allowChildren"))]
pub allow_children: u8,
pub expandable: u8,
pub leaf: u8,
pub id: String,
pub text: String,
}Parameters for render API:
#[derive(Default, Debug, Deserialize)]
pub struct RenderRequest {
/// Target metric path
pub target: Option<String>,
/// Start time
pub from: Option<String>,
/// End time
pub until: Option<String>,
/// Maximum data points to return
#[serde(rename(deserialize = "maxDataPoints"))]
pub max_data_points: Option<u16>,
}pub fn get_graphite_routes() -> Router<AppState> {
Router::new()
.route("/functions", get(handler_functions))
.route("/metrics/find", get(handler_metrics_find_get).post(handler_metrics_find_post))
.route("/render", get(handler_render).post(handler_render))
.route("/tags/autoComplete/tags", get(handler_tags))
}| Method | Path | Handler | Description |
|---|---|---|---|
| GET | /functions |
handler_functions |
Returns empty object (Grafana compatibility) |
| GET/POST | /metrics/find |
handler_metrics_find_* |
Discover available metrics |
| GET/POST | /render |
handler_render |
Render time series data |
| GET | /tags/autoComplete/tags |
handler_tags |
Returns empty array (Grafana compatibility) |
The module exposes a virtual metric hierarchy:
├── flag
│ └── {environment}
│ └── {service}
│ └── {metric_name}
└── health
└── {environment}
└── {service_name}
pub fn find_metrics(find_request: MetricsQuery, state: AppState) -> Vec<Metric>Query patterns:
*- Returns top-level:["flag", "health"]flag.*orhealth.*- Returns environmentsflag.{env}.*- Returns servicesflag.{env}.{service}.*- Returns metric nameshealth.{env}.*- Returns health metric names
Handles both GET and POST requests for time series data.
Flag Metrics (flag.{env}.{service}.{metric}):
- Looks up metric configuration
- Queries upstream Graphite with resolved query
- Converts raw values to binary flags (0/1) based on threshold
Health Metrics (health.{env}.{service}):
- Calls
get_service_health()from common module - Returns aggregated health scores
[
{
"target": "service.metric-name",
"datapoints": [[1.0, 1704067200], [0.0, 1704070800]]
}
]Core function for querying upstream Graphite:
pub async fn get_graphite_data(
client: &reqwest::Client,
url: &str,
targets: &HashMap<String, String>, // alias -> query
from: Option<DateTime<FixedOffset>>,
from_raw: Option<String>,
to: Option<DateTime<FixedOffset>>,
to_raw: Option<String>,
max_data_points: u16,
) -> Result<Vec<GraphiteData>, CloudMonError>Query Construction:
let query_params: Vec<(_, String)> = [
("format", "json".to_string()),
("maxDataPoints", max_data_points.to_string()),
].into();
// Add from, until, and target parametersQuery Aliasing:
fn alias_graphite_query(query: &str, alias: &str) -> String {
format!("alias({},'{}')", query, alias)
}This wraps each query with Graphite's alias() function to preserve the logical metric name in responses.
Custom Axum extractor that accepts both JSON and form-encoded bodies:
#[derive(Default, Debug)]
pub struct JsonOrForm<T>(T);
#[async_trait]
impl<S, B, T> FromRequest<S, B> for JsonOrForm<T>
where
// ... constraints
{
async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejection> {
let content_type = req.headers().get(CONTENT_TYPE);
if content_type.starts_with("application/json") {
// Extract as JSON
}
if content_type.starts_with("application/x-www-form-urlencoded") {
// Extract as Form
}
Err(StatusCode::UNSUPPORTED_MEDIA_TYPE)
}
}This enables compatibility with various Graphite clients (Grafana uses form encoding).
Grafana/Client
│
▼
┌─────────────────┐
│ /metrics/find │──► find_metrics() ──► AppState.flag_metrics
└─────────────────┘ AppState.health_metrics
AppState.environments
│
▼
┌─────────────────┐
│ /render │──► handler_render()
└─────────────────┘
│
├──► Flag: get_graphite_data() ──► Upstream Graphite
│ │
│ ▼
│ Convert to 0/1 flags
│
└──► Health: get_service_health() ──► Aggregate expressions
- Returns empty array
[]for unrecognized metric paths - Returns
CloudMonError::GraphiteErrorfor upstream failures - Logs warnings for unknown targets in responses