Skip to content

Commit e8dc416

Browse files
committed
Refactor to move caching, metrics, etc from v1 to common dir
1 parent f871540 commit e8dc416

File tree

11 files changed

+292
-222
lines changed

11 files changed

+292
-222
lines changed
Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,35 @@ use actix_web::{
22
HttpResponse,
33
body::{BoxBody, EitherBody, MessageBody},
44
dev::{ServiceRequest, ServiceResponse},
5-
http::header::{ETAG, HeaderValue, IF_NONE_MATCH},
5+
http::header::{ETAG, HeaderMap, HeaderValue, IF_NONE_MATCH},
66
middleware::Next,
7-
web
7+
web::{self, Bytes}
88
};
99
use prometheus_client::encoding::EncodeLabelSet;
1010
use sha2::{Digest as _, Sha256};
1111

12-
use super::{ApiData, CacheKey, CacheValue};
12+
use crate::api::common::data::ApiData;
1313

1414
#[derive(Debug, Hash, PartialEq, Eq, Clone, EncodeLabelSet)]
1515
pub struct CacheLabels {
1616
endpoint: String
1717
}
1818

19+
#[derive(Hash, PartialEq, Eq, Clone)]
20+
pub struct CacheKey {
21+
pub path: String,
22+
pub query: String
23+
}
24+
25+
pub type ETagType = [u8; 32];
26+
27+
#[derive(Clone)]
28+
pub struct CacheValue {
29+
pub response: Bytes,
30+
pub headers: HeaderMap,
31+
pub etag: ETagType
32+
}
33+
1934
pub async fn middleware(
2035
service_request: ServiceRequest,
2136
next: Next<impl MessageBody>
@@ -66,6 +81,7 @@ pub async fn middleware(
6681
// Record a cache hit to the metrics
6782
app_data
6883
.metrics
84+
.global
6985
.cache_hits
7086
.get_or_create(&metric_labels)
7187
.inc();
@@ -104,6 +120,7 @@ pub async fn middleware(
104120
// Record a cache miss to the metrics
105121
app_data
106122
.metrics
123+
.global
107124
.cache_misses
108125
.get_or_create(&metric_labels)
109126
.inc();

src/api/common/data.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::{collections::HashSet, sync::Arc, time::Duration};
2+
3+
use moka::future::Cache;
4+
5+
use super::caching::{CacheKey, CacheValue};
6+
use crate::{
7+
AppCommand,
8+
api::common::{caching::ETagType, metrics::AppMetrics}
9+
};
10+
11+
pub struct ApiData {
12+
/// The maven URL prefix to expose publicly, for example https://repo.polyfrost.org/
13+
pub public_maven_url: String,
14+
/// The maven URL prefix to resolve artifacts internally, for example https://172.19.0.3:8080/
15+
pub internal_maven_url: String,
16+
/// A reqwest client to use to fetch maven data
17+
pub client: Arc<reqwest::Client>,
18+
/// The allowlist of paths that should be cached
19+
pub cache_allowlist: HashSet<&'static str>,
20+
/// The internal cache used to cache artifact responses.
21+
pub cache: Cache<CacheKey, CacheValue>,
22+
/// All the metrics objects used for encoding and recording metrics
23+
pub metrics: AppMetrics
24+
}
25+
26+
impl ApiData {
27+
pub fn new(args: &AppCommand) -> Self {
28+
Self {
29+
public_maven_url: args.public_maven_url.to_string(),
30+
internal_maven_url: args
31+
.internal_maven_url
32+
.as_ref()
33+
.map_or(args.public_maven_url.to_string(), |u| u.to_string()),
34+
client: reqwest::ClientBuilder::new()
35+
.user_agent(concat!(
36+
env!("CARGO_PKG_NAME"),
37+
"/",
38+
env!("CARGO_PKG_VERSION"),
39+
" (",
40+
env!("CARGO_PKG_REPOSITORY"),
41+
")"
42+
))
43+
.build()
44+
.unwrap()
45+
.into(),
46+
cache_allowlist: HashSet::from([
47+
"/v1/artifacts/oneconfig",
48+
"/v1/artifacts/{artifact:stage1|relaunch}"
49+
]),
50+
cache: Cache::builder()
51+
.time_to_live(Duration::from_mins(2))
52+
.weigher(|k: &CacheKey, v: &CacheValue| {
53+
(k.path.len()
54+
+ k.query.len() + const { std::mem::size_of::<ETagType>() }
55+
+ v.response.len() + std::mem::size_of_val(&v.headers))
56+
.try_into()
57+
.unwrap_or(u32::MAX)
58+
})
59+
.max_capacity(/* 10 MiB */ const { 10 * 1024 * 1024 })
60+
.build(),
61+
metrics: AppMetrics::new()
62+
}
63+
}
64+
}

src/api/common/metrics.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
use actix_web::{
2+
HttpResponse,
3+
Responder,
4+
body::MessageBody,
5+
dev::{ServiceRequest, ServiceResponse},
6+
get,
7+
middleware::Next,
8+
web::{self, ServiceConfig}
9+
};
10+
use documented::DocumentedFields;
11+
use prometheus_client::{
12+
encoding::{EncodeLabelSet, text::encode},
13+
metrics::{counter::Counter, family::Family},
14+
registry::Registry
15+
};
16+
17+
use crate::api::{
18+
common::{caching::CacheLabels, data::ApiData},
19+
v1::metrics::ApiV1Metrics
20+
};
21+
22+
// TODO: Improve this macro so you can use only one macro call and it will
23+
// register all of them
24+
25+
/// A macro that automatically initializes and registers a metric using inferred
26+
/// types and doc comments from the ApiMetrics struct
27+
#[macro_export]
28+
macro_rules! make_api_metric {
29+
($registry:expr, $metrics_struct:ident, $name:ident) => {
30+
make_api_metric!($registry, $metrics_struct, $name, Family)
31+
};
32+
($registry:expr, $metrics_struct:ident, $name:ident, $type:ident) => {
33+
let $name = $type::default();
34+
let name_str = stringify!($name);
35+
$registry.register(
36+
name_str,
37+
$metrics_struct::get_field_docs(name_str)
38+
.expect(&format!("No doc comment for '{}' field", name_str)),
39+
$name.clone()
40+
);
41+
};
42+
}
43+
44+
pub trait MetricsGroup {
45+
fn init_metrics(registry: &mut Registry) -> Self;
46+
}
47+
48+
pub struct AppMetrics {
49+
/// The root metrics registry used for storing and retrieving metrics
50+
pub registry: Registry,
51+
/// All of the global (application-wide) metrics
52+
pub global: GlobalMetrics,
53+
/// All of the API v1 metrics
54+
pub v1: ApiV1Metrics
55+
}
56+
57+
impl AppMetrics {
58+
pub fn new() -> AppMetrics {
59+
let mut registry = <Registry>::default();
60+
61+
AppMetrics {
62+
global: GlobalMetrics::init_metrics(&mut registry),
63+
v1: ApiV1Metrics::init_metrics(&mut registry),
64+
registry
65+
}
66+
}
67+
}
68+
69+
#[derive(DocumentedFields)]
70+
pub struct GlobalMetrics {
71+
/// The amount of generic API requests by endpoint and status code
72+
api_requests: Family<ApiRequestLabels, Counter>,
73+
/// The amount of cache hits by endpoint
74+
pub cache_hits: Family<CacheLabels, Counter>,
75+
/// The amount of cache misses by endpoint
76+
pub cache_misses: Family<CacheLabels, Counter>
77+
}
78+
79+
impl MetricsGroup for GlobalMetrics {
80+
fn init_metrics(registry: &mut Registry) -> Self {
81+
make_api_metric!(registry, Self, api_requests);
82+
make_api_metric!(registry, Self, cache_hits);
83+
make_api_metric!(registry, Self, cache_misses);
84+
85+
Self {
86+
api_requests,
87+
cache_hits,
88+
cache_misses
89+
}
90+
}
91+
}
92+
93+
pub fn configure(config: &mut ServiceConfig) { config.service(metrics_endpoint); }
94+
95+
/// The endpoint to allow scraping metrics
96+
#[get("/metrics")]
97+
async fn metrics_endpoint(state: web::Data<ApiData>) -> impl Responder {
98+
let mut body = String::new();
99+
if let Err(e) = encode(&mut body, &state.metrics.registry) {
100+
return HttpResponse::InternalServerError()
101+
.content_type("text/plain")
102+
.body(format!("Error encoding metrics: {e}"));
103+
}
104+
105+
HttpResponse::Ok()
106+
.content_type("application/openmetrics-text; version=1.0.0; charset=utf-8")
107+
.body(body)
108+
}
109+
110+
#[derive(Debug, Hash, PartialEq, Eq, Clone, EncodeLabelSet)]
111+
struct ApiRequestLabels {
112+
path: String,
113+
status_code: u16
114+
}
115+
116+
pub async fn middleware(
117+
mut service_request: ServiceRequest,
118+
next: Next<impl MessageBody>
119+
) -> Result<ServiceResponse<impl MessageBody>, actix_web::Error> {
120+
let data = service_request.extract::<web::Data<ApiData>>().await?;
121+
122+
let path = service_request.uri().path().to_string();
123+
let response = next.call(service_request).await;
124+
125+
let labels = match &response {
126+
Ok(r) => ApiRequestLabels {
127+
path,
128+
status_code: r.status().as_u16()
129+
},
130+
Err(e) => ApiRequestLabels {
131+
path,
132+
status_code: e.as_response_error().status_code().as_u16()
133+
}
134+
};
135+
data.metrics
136+
.global
137+
.api_requests
138+
.get_or_create(&labels)
139+
.inc();
140+
141+
response
142+
}

src/api/common/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod caching;
2+
pub mod data;
3+
pub mod metrics;

src/api/legacy/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

src/api/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1+
pub mod common;
2+
3+
pub mod legacy;
14
pub mod v1;

src/api/v1/endpoints/artifacts.rs

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@ use serde::{Deserialize, Serialize};
1111
use tokio::task::JoinSet;
1212

1313
use crate::{
14-
api::v1::{
15-
ApiData,
16-
responses::{ArtifactResponse, Checksum, ChecksumType, ErrorResponse, consts::*}
14+
api::{
15+
common::data::ApiData,
16+
v1::responses::{
17+
ArtifactResponse,
18+
Checksum,
19+
ChecksumType,
20+
ErrorResponse,
21+
consts::*
22+
}
1723
},
1824
maven::{self, MavenError},
1925
types::gradle_module_metadata::{
@@ -27,14 +33,12 @@ use crate::{
2733

2834
const ONECONFIG_GROUP: &str = "org.polyfrost.oneconfig";
2935

30-
pub fn configure() -> impl FnOnce(&mut ServiceConfig) {
31-
|config| {
32-
config.service(
33-
web::scope("/artifacts")
34-
.service(oneconfig)
35-
.service(platform_agnostic_artifacts)
36-
);
37-
}
36+
pub fn configure(config: &mut ServiceConfig) {
37+
config.service(
38+
web::scope("/artifacts")
39+
.service(oneconfig)
40+
.service(platform_agnostic_artifacts)
41+
);
3842
}
3943

4044
#[derive(Serialize, Deserialize, Debug, Hash, PartialEq, Eq, Clone)]
@@ -190,10 +194,6 @@ async fn oneconfig(
190194
};
191195

192196
let mut join_set: JoinSet<Result<ArtifactResponse, anyhow::Error>> = JoinSet::new();
193-
let internal_maven_url = state
194-
.internal_maven_url
195-
.clone()
196-
.unwrap_or(state.public_maven_url.clone());
197197

198198
for variant in dependency.variants {
199199
let Variant::OneConfigModulesApiElements { dependencies } = variant else {
@@ -206,7 +206,7 @@ async fn oneconfig(
206206
}
207207

208208
let internal_dep_url =
209-
maven::get_dep_url(&internal_maven_url, repository, &dep);
209+
maven::get_dep_url(&state.internal_maven_url, repository, &dep);
210210
let dep_url = maven::get_dep_url(&state.public_maven_url, repository, &dep);
211211

212212
let client = state.client.clone();
@@ -295,14 +295,7 @@ async fn platform_agnostic_artifacts(
295295

296296
let checksum = match maven::fetch_checksum(
297297
&state.client,
298-
&maven::get_dep_url(
299-
&state
300-
.internal_maven_url
301-
.clone()
302-
.unwrap_or(state.public_maven_url.clone()),
303-
repository,
304-
&dep
305-
)
298+
&maven::get_dep_url(&state.internal_maven_url, repository, &dep)
306299
)
307300
.await
308301
{

0 commit comments

Comments
 (0)