Skip to content

Commit a2792be

Browse files
committed
Add caching & http status code information to metrics
1 parent c9248d0 commit a2792be

4 files changed

Lines changed: 108 additions & 49 deletions

File tree

src/api/v1/caching.rs

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::HashSet;
2+
13
use actix_web::{
24
HttpResponse,
35
body::{BoxBody, EitherBody, MessageBody},
@@ -6,26 +8,41 @@ use actix_web::{
68
middleware::Next,
79
web
810
};
11+
use prometheus_client::encoding::EncodeLabelSet;
912
use sha2::{Digest as _, Sha256};
1013

1114
use super::{ApiData, CacheKey, CacheValue};
1215

16+
#[derive(Debug, Hash, PartialEq, Eq, Clone, EncodeLabelSet)]
17+
pub struct CacheLabels {
18+
endpoint: String
19+
}
20+
1321
pub async fn middleware(
1422
service_request: ServiceRequest,
1523
next: Next<impl MessageBody>
1624
) -> Result<ServiceResponse<EitherBody<impl MessageBody>>, actix_web::Error> {
17-
let cache = match service_request.app_data::<web::Data<ApiData>>() {
18-
Some(app_data) => app_data.cache.clone(),
19-
None => {
20-
// If we don't have ApiData for whatever reason, we can't do much
21-
// cache-related Technically this could probably be an unwrap, but this is
22-
// cleaner
23-
return next
24-
.call(service_request)
25-
.await
26-
.map(|resp| resp.map_into_left_body());
27-
}
25+
let match_pattern = service_request.match_pattern().unwrap_or("default".to_string());
26+
let Some(app_data) = service_request.app_data::<web::Data<ApiData>>() else {
27+
// If we don't have ApiData for whatever reason, we can't do much
28+
// cache-related Technically this could probably be an unwrap, but this is
29+
// cleaner
30+
return next
31+
.call(service_request)
32+
.await
33+
.map(|resp| resp.map_into_left_body());
2834
};
35+
36+
// Check whether the endpoint should actually be cached
37+
if !app_data.cache_allowlist.contains(match_pattern.as_str()) {
38+
return next
39+
.call(service_request)
40+
.await
41+
.map(|resp| resp.map_into_left_body());
42+
}
43+
44+
let cache = app_data.cache.clone();
45+
let metric_labels = CacheLabels { endpoint: match_pattern };
2946
let cache_key = CacheKey {
3047
path: service_request.path().to_string(),
3148
query: service_request.query_string().to_string()
@@ -44,6 +61,9 @@ pub async fn middleware(
4461

4562
// Resolve cache entry with path & query
4663
if let Some(cache_value) = cache.get(&cache_key).await {
64+
// Record a cache hit to the metrics
65+
app_data.metrics.cache_hits.get_or_create(&metric_labels).inc();
66+
4767
// Short circuit with HttpResponse::NotModified() if the If-None-Match header
4868
// matches cache
4969
if let Some((if_none_match, etag)) = if_none_match
@@ -74,6 +94,9 @@ pub async fn middleware(
7494
}
7595

7696
return Ok(service_request.into_response(res).map_into_right_body());
97+
} else {
98+
// Record a cache miss to the metrics
99+
app_data.metrics.cache_misses.get_or_create(&metric_labels).inc();
77100
}
78101

79102
// If none of the caching cases were handled, pass through to other handlers

src/api/v1/metrics.rs

Lines changed: 65 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ use prometheus_client::{
1515
};
1616

1717
use crate::api::v1::{
18-
ApiData,
19-
endpoints::artifacts::{ArtifactQuery, OneConfigVersionInfo}
18+
caching::CacheLabels, endpoints::artifacts::{ArtifactQuery, OneConfigVersionInfo}, ApiData
2019
};
2120

2221
/// A macro that automatically initializes and registers a metric using inferred
@@ -37,6 +36,12 @@ macro_rules! make_api_metric {
3736
};
3837
}
3938

39+
#[derive(Debug, Hash, PartialEq, Eq, Clone, EncodeLabelSet)]
40+
struct ApiRequestLabels {
41+
path: String,
42+
status_code: u16
43+
}
44+
4045
#[derive(Debug, Hash, PartialEq, Eq, Clone, EncodeLabelSet)]
4146
struct PlatformAgnosticArtifactLabels {
4247
r#type: String
@@ -47,10 +52,16 @@ struct PlatformAgnosticArtifactLabels {
4752
pub struct ApiMetrics {
4853
/// The registry used for storing metrics
4954
registry: Registry,
55+
/// The generic amount of API requests by path and response code
56+
api_requests: Family<ApiRequestLabels, Counter>,
5057
/// The amount of OneConfig artifacts requests, by version and loader
5158
oneconfig_artifacts_requests: Family<OneConfigVersionInfo, Counter>,
5259
/// The amount of platform-agnostic artifacts requests, by type
53-
platform_agnostic_artifacts_requests: Family<PlatformAgnosticArtifactLabels, Counter>
60+
platform_agnostic_artifacts_requests: Family<PlatformAgnosticArtifactLabels, Counter>,
61+
/// The amount of cache hits by endpoint
62+
pub cache_hits: Family<CacheLabels, Counter>,
63+
/// The amount of cache misses by endpoint
64+
pub cache_misses: Family<CacheLabels, Counter>
5465
}
5566

5667
/// Configures the metrics endpoint. In addition to this, the metrics middleware
@@ -66,13 +77,19 @@ pub fn configure() -> impl FnOnce(&mut ServiceConfig) {
6677
pub fn init_metrics() -> ApiMetrics {
6778
let mut registry = <Registry>::default();
6879

80+
make_api_metric!(registry, api_requests);
6981
make_api_metric!(registry, oneconfig_artifacts_requests);
7082
make_api_metric!(registry, platform_agnostic_artifacts_requests);
83+
make_api_metric!(registry, cache_hits);
84+
make_api_metric!(registry, cache_misses);
7185

7286
ApiMetrics {
7387
registry,
88+
api_requests,
7489
oneconfig_artifacts_requests,
75-
platform_agnostic_artifacts_requests
90+
platform_agnostic_artifacts_requests,
91+
cache_hits,
92+
cache_misses
7693
}
7794
}
7895

@@ -96,37 +113,50 @@ pub async fn middleware(
96113
mut service_request: ServiceRequest,
97114
next: Next<impl MessageBody>
98115
) -> Result<ServiceResponse<impl MessageBody>, actix_web::Error> {
99-
if let Some(pattern) = service_request.match_pattern() {
100-
let data = service_request.extract::<web::Data<ApiData>>().await?;
101-
102-
match pattern.as_str() {
103-
"/v1/artifacts/oneconfig" => {
104-
data.metrics
105-
.oneconfig_artifacts_requests
106-
.get_or_create(
107-
&service_request
108-
.extract::<web::Query<ArtifactQuery<OneConfigVersionInfo>>>()
109-
.await?
110-
.version_info
111-
)
112-
.inc();
113-
}
114-
"/v1/artifacts/{artifact:stage1|relaunch}" => {
115-
data.metrics
116-
.platform_agnostic_artifacts_requests
117-
.get_or_create(&PlatformAgnosticArtifactLabels {
118-
// Unfortunately actix makes it difficult to extract the real
119-
// parsed URL parameter, so just substring instead as a substitute
120-
r#type: service_request.uri().path()
121-
[const { "/v1/artifacts/".len() }..]
122-
.to_string()
123-
})
124-
.inc();
125-
}
126-
_ => ()
127-
};
128-
}
116+
let data = service_request.extract::<web::Data<ApiData>>().await?;
117+
118+
match service_request.match_pattern().unwrap_or("default".to_string()).as_str() {
119+
"/v1/artifacts/oneconfig" => {
120+
data.metrics
121+
.oneconfig_artifacts_requests
122+
.get_or_create(
123+
&service_request
124+
.extract::<web::Query<ArtifactQuery<OneConfigVersionInfo>>>()
125+
.await?
126+
.version_info
127+
)
128+
.inc();
129+
}
130+
"/v1/artifacts/{artifact:stage1|relaunch}" => {
131+
data.metrics
132+
.platform_agnostic_artifacts_requests
133+
.get_or_create(&PlatformAgnosticArtifactLabels {
134+
// Unfortunately actix makes it difficult to extract the real
135+
// parsed URL parameter, so just substring instead as a substitute
136+
r#type: service_request.uri().path()
137+
[const { "/v1/artifacts/".len() }..]
138+
.to_string()
139+
})
140+
.inc();
141+
}
142+
_ => ()
143+
};
129144

130145
// Let the real request handler continue
131-
return next.call(service_request).await;
146+
let path = service_request.uri().path().to_string();
147+
let response = next.call(service_request).await;
148+
149+
let labels = match &response {
150+
Ok(r) => ApiRequestLabels {
151+
path,
152+
status_code: r.status().as_u16()
153+
},
154+
Err(e) => ApiRequestLabels {
155+
path,
156+
status_code: e.as_response_error().status_code().as_u16()
157+
}
158+
};
159+
data.metrics.api_requests.get_or_create(&labels).inc();
160+
161+
response
132162
}

src/api/v1/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pub mod endpoints;
33
pub mod metrics;
44
pub mod responses;
55

6-
use std::sync::Arc;
6+
use std::{collections::HashSet, sync::Arc};
77

88
use actix_web::{
99
http::header::HeaderMap,
@@ -35,10 +35,12 @@ pub struct ApiData {
3535
pub internal_maven_url: Option<String>,
3636
/// A reqwest client to use to fetch maven data
3737
pub client: Arc<reqwest::Client>,
38+
/// The allowlist of paths that should be cached
39+
pub cache_allowlist: HashSet<&'static str>,
3840
/// The internal cache used to cache artifact responses.
3941
pub cache: Cache<CacheKey, CacheValue>,
4042
/// All the metrics objects used for encoding and recording metrics
41-
pub metrics: ApiMetrics
43+
pub metrics: ApiMetrics,
4244
}
4345

4446
pub fn configure() -> impl FnOnce(&mut ServiceConfig) {

src/main.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ mod api;
44
mod maven;
55
mod types;
66

7-
use std::{net::SocketAddr, time::Duration};
7+
use std::{collections::HashSet, net::SocketAddr, time::Duration};
88

99
use actix_web::{App, HttpServer, web};
1010
use api::v1::{ApiData, CacheKey, CacheValue, ETagType};
@@ -66,6 +66,10 @@ async fn main() {
6666
.build()
6767
.unwrap()
6868
.into(),
69+
cache_allowlist: HashSet::from([
70+
"/v1/artifacts/oneconfig",
71+
"/v1/artifacts/{artifact:stage1|relaunch}"
72+
]),
6973
cache: Cache::builder()
7074
.time_to_live(Duration::from_mins(2))
7175
.weigher(|k: &CacheKey, v: &CacheValue| {

0 commit comments

Comments
 (0)