Skip to content

Commit 0ccb6d0

Browse files
committed
Make metrics and prometheus features optional
Introduce `metrics` and `prometheus` feature flags to allow users to opt out of telemetry dependencies. Update the `Registry` and `CacheManager` to conditionally compile metrics logic, and adjust documentation and tests to reflect these changes.
1 parent 200b616 commit 0ccb6d0

File tree

11 files changed

+191
-65
lines changed

11 files changed

+191
-65
lines changed

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ license = "GPL-3.0"
99
name = "jwks-cache"
1010
readme = "README.md"
1111
repository = "https://github.com/hack-ink/jwks-cache"
12-
version = "0.1.5"
12+
version = "0.2.0"
1313

1414
[package.metadata.docs.rs]
1515
all-features = true
@@ -23,7 +23,14 @@ inherits = "release"
2323
lto = true
2424

2525
[features]
26-
default = []
26+
metrics = [
27+
"dep:metrics",
28+
"smallvec",
29+
]
30+
prometheus = [
31+
"metrics",
32+
"metrics-exporter-prometheus",
33+
]
2734

2835
[dependencies]
2936
# crates.io
@@ -33,19 +40,19 @@ http = { version = "1.4" }
3340
http-cache-semantics = { version = "2.1" }
3441
httpdate = { version = "1.0" }
3542
jsonwebtoken = { version = "10.2", features = ["aws_lc_rs"] }
36-
metrics = { version = "0.24" }
37-
metrics-exporter-prometheus = { version = "0.18" }
43+
metrics = { version = "0.24", optional = true }
44+
metrics-exporter-prometheus = { version = "0.18", optional = true }
3845
rand = { version = "0.9", features = ["small_rng", "std"] }
3946
redis = { version = "0.32", optional = true, default-features = false, features = ["aio", "tokio-comp"] }
4047
reqwest = { version = "0.12", default-features = false, features = ["http2", "json", "rustls-tls", "stream"] }
4148
serde = { version = "1.0", features = ["derive"] }
4249
serde_json = { version = "1.0" }
4350
sha2 = { version = "0.10" }
44-
smallvec = { version = "1.15" }
51+
smallvec = { version = "1.15", optional = true }
4552
thiserror = { version = "2.0" }
4653
tokio = { version = "1.48", features = ["macros", "rt-multi-thread", "sync", "time"] }
4754
tracing = { version = "0.1" }
48-
url = { version = "2.5" }
55+
url = { version = "2.5", features = ["serde"] }
4956

5057
[dev-dependencies]
5158
# crates.io

Makefile.toml

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
# Format
2-
# | task | type | cwd |
3-
# | ---- | ------- | --- |
4-
# | fmt | command | |
5-
# | fmt-check | command | |
2+
# | task | type | cwd |
3+
# | ------------ | --------- | --- |
4+
# | fmt | composite | |
5+
# | fmt-check | composite | |
6+
# | fmt-rust | script | |
7+
# | fmt-rust-check| extend | |
68

79
[tasks.fmt]
810
workspace = false
9-
command = "cargo"
10-
args = ["fmt"]
11+
dependencies = ["fmt-rust"]
1112

1213
[tasks.fmt-check]
1314
workspace = false
14-
command = "cargo"
15-
args = [
16-
"fmt",
17-
"--",
18-
"--check"
19-
]
15+
dependencies = ["fmt-rust-check"]
16+
17+
[tasks.fmt-rust]
18+
workspace = false
19+
script = "cargo +nightly fmt ${FMT_RUST_ARGS}"
20+
env = { FMT_RUST_ARGS = "" }
21+
22+
[tasks.fmt-rust-check]
23+
workspace = false
24+
extend = "fmt-rust"
25+
env = { FMT_RUST_ARGS = "-- --check" }
2026

2127

2228
# Lint

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ The crate is fully async and designed for the Tokio multi-threaded runtime.
6060
#[tokio::main]
6161
async fn main() -> anyhow::Result<()> {
6262
tracing_subscriber::fmt::init();
63-
// Optional Prometheus exporter (metrics are always sent via the `metrics` facade).
63+
// Optional Prometheus exporter (requires the `prometheus` feature).
6464
jwks_cache::install_default_exporter()?;
6565

6666
let registry = jwks_cache::Registry::builder()
@@ -144,7 +144,7 @@ The optional third argument to `Registry::resolve` lets you pass the `kid` up fr
144144
- `register` / `unregister` keep provider state scoped to each tenant.
145145
- `resolve` serves cached JWKS payloads with per-tenant metrics tagging.
146146
- `refresh` triggers an immediate background refresh without waiting for TTL expiry.
147-
- `provider_status` and `all_statuses` expose lifecycle state, expiry, error counters, hit rates, and the metrics that power `jwks-cache.openapi.yaml`.
147+
- `provider_status` and `all_statuses` expose lifecycle state, expiry, and error counters, plus hit rates and status metrics when the `metrics` feature is enabled.
148148

149149
### Security controls
150150

@@ -154,12 +154,15 @@ The optional third argument to `Registry::resolve` lets you pass the `kid` up fr
154154

155155
### Feature flags
156156

157-
- `redis`: enable Redis-backed snapshots for `persist_all` and `restore_from_persistence`. When disabled, these methods are cheap no-ops so lifecycle code can stay shared.
157+
- The `redis` feature enables Redis-backed snapshots for `persist_all` and `restore_from_persistence`. When disabled, these methods are cheap no-ops so lifecycle code can stay shared.
158+
- The `metrics` feature enables metrics emission through the `metrics` facade.
159+
- The `prometheus` feature enables `install_default_exporter` to install the bundled Prometheus recorder (implies `metrics`).
160+
- The default features include `prometheus` and `metrics`; disable them with `default-features = false`.
158161

159162
## Observability
160163

161-
- Metrics emitted via the `metrics` facade include `jwks_cache_requests_total`, `jwks_cache_hits_total`, `jwks_cache_misses_total`, `jwks_cache_stale_total`, `jwks_cache_refresh_total`, `jwks_cache_refresh_errors_total`, and the `jwks_cache_refresh_duration_seconds` histogram.
162-
- `install_default_exporter` installs the bundled Prometheus recorder (`metrics-exporter-prometheus`) and exposes a `PrometheusHandle` for HTTP servers to serve `/metrics`.
164+
- Metrics emitted via the `metrics` facade (requires the `metrics` feature) include `jwks_cache_requests_total`, `jwks_cache_hits_total`, `jwks_cache_misses_total`, `jwks_cache_stale_total`, `jwks_cache_refresh_total`, `jwks_cache_refresh_errors_total`, and the `jwks_cache_refresh_duration_seconds` histogram.
165+
- The `install_default_exporter` function installs the bundled Prometheus recorder (`metrics-exporter-prometheus`) and exposes a `PrometheusHandle` for HTTP servers to serve `/metrics` (requires the `prometheus` feature).
163166
- Every cache operation is instrumented with `tracing` spans keyed by tenant and provider identifiers, making it easy to correlate logs, traces, and metrics.
164167

165168
## Persistence & Warm Starts

docs/spec/architecture.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ Scope: JWKS caching behavior, registry lifecycle, HTTP semantics, persistence, m
3838

3939
## Metrics and tracing
4040

41-
- Metrics flow through the `metrics` facade.
42-
- `install_default_exporter` installs the bundled Prometheus recorder.
41+
- Metrics flow through the `metrics` facade when the `metrics` feature is enabled.
42+
- The `install_default_exporter` function installs the bundled Prometheus recorder when the `prometheus` feature is enabled.
4343
- Cache operations emit structured `tracing` spans keyed by tenant and provider identifiers.
4444

4545
## Security and validation
@@ -81,7 +81,7 @@ Handles JWKS fetches, retry policies, and HTTP caching semantics.
8181

8282
### `metrics`
8383

84-
Captures per-provider metrics and exposes Prometheus-compatible exporters.
84+
Captures per-provider metrics and exposes Prometheus-compatible exporters when enabled.
8585

8686
### `security`
8787

src/cache/manager.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use tokio::{
1515
time,
1616
};
1717
// self
18+
#[cfg(feature = "metrics")] use crate::metrics::{self, ProviderMetrics};
1819
#[cfg(feature = "redis")] use crate::registry::PersistentSnapshot;
1920
use crate::{
2021
_prelude::*,
@@ -27,7 +28,6 @@ use crate::{
2728
retry::{AttemptBudget, RetryExecutor},
2829
semantics::{Freshness, base_request, evaluate_freshness, evaluate_revalidation},
2930
},
30-
metrics::{self, ProviderMetrics},
3131
registry::IdentityProviderRegistration,
3232
};
3333

@@ -41,6 +41,7 @@ pub struct CacheManager {
4141
client: Arc<Client>,
4242
entry: Arc<RwLock<CacheEntry>>,
4343
single_flight: Arc<Mutex<()>>,
44+
#[cfg(feature = "metrics")]
4445
metrics: Arc<ProviderMetrics>,
4546
}
4647
impl CacheManager {
@@ -54,14 +55,25 @@ impl CacheManager {
5455
.connect_timeout(Duration::from_secs(5))
5556
.build()?;
5657

57-
Ok(Self::with_parts(registration, client, ProviderMetrics::new()))
58+
#[cfg(feature = "metrics")]
59+
let manager = Self::with_parts(registration, client, ProviderMetrics::new());
60+
#[cfg(not(feature = "metrics"))]
61+
let manager = Self::with_parts(registration, client);
62+
63+
Ok(manager)
5864
}
5965

6066
/// Build a cache manager using the supplied HTTP client (primarily for tests).
6167
pub fn with_client(registration: IdentityProviderRegistration, client: Client) -> Self {
62-
Self::with_parts(registration, client, ProviderMetrics::new())
68+
#[cfg(feature = "metrics")]
69+
let manager = Self::with_parts(registration, client, ProviderMetrics::new());
70+
#[cfg(not(feature = "metrics"))]
71+
let manager = Self::with_parts(registration, client);
72+
73+
manager
6374
}
6475

76+
#[cfg(feature = "metrics")]
6577
fn with_parts(
6678
registration: IdentityProviderRegistration,
6779
client: Client,
@@ -79,7 +91,21 @@ impl CacheManager {
7991
}
8092
}
8193

94+
#[cfg(not(feature = "metrics"))]
95+
fn with_parts(registration: IdentityProviderRegistration, client: Client) -> Self {
96+
let tenant = registration.tenant_id.clone();
97+
let provider = registration.provider_id.clone();
98+
99+
Self {
100+
registration: Arc::new(registration),
101+
client: Arc::new(client),
102+
entry: Arc::new(RwLock::new(CacheEntry::new(tenant, provider))),
103+
single_flight: Arc::new(Mutex::new(())),
104+
}
105+
}
106+
82107
/// Access the per-provider metrics accumulator.
108+
#[cfg(feature = "metrics")]
83109
pub fn metrics(&self) -> Arc<ProviderMetrics> {
84110
self.metrics.clone()
85111
}
@@ -201,14 +227,17 @@ impl CacheManager {
201227
match self.refresh_blocking(true).await? {
202228
RefreshOutcome::Updated { jwks, from_cache } => {
203229
if from_cache {
230+
#[cfg(feature = "metrics")]
204231
self.observe_hit(false);
205232
} else {
233+
#[cfg(feature = "metrics")]
206234
self.observe_miss();
207235
}
208236

209237
return Ok(jwks);
210238
},
211239
RefreshOutcome::Stale(jwks) => {
240+
#[cfg(feature = "metrics")]
212241
self.observe_hit(true);
213242

214243
return Ok(jwks);
@@ -219,6 +248,7 @@ impl CacheManager {
219248
if !payload.is_expired(now) {
220249
let jwks = payload.jwks.clone();
221250

251+
#[cfg(feature = "metrics")]
222252
self.observe_hit(false);
223253

224254
if now >= payload.next_refresh_at {
@@ -234,14 +264,17 @@ impl CacheManager {
234264
match self.refresh_blocking(false).await {
235265
Ok(RefreshOutcome::Updated { jwks, from_cache }) => {
236266
if from_cache {
267+
#[cfg(feature = "metrics")]
237268
self.observe_hit(false);
238269
} else {
270+
#[cfg(feature = "metrics")]
239271
self.observe_miss();
240272
}
241273

242274
return Ok(jwks);
243275
},
244276
Ok(RefreshOutcome::Stale(jwks)) => {
277+
#[cfg(feature = "metrics")]
245278
self.observe_hit(true);
246279

247280
return Ok(jwks);
@@ -250,6 +283,7 @@ impl CacheManager {
250283
if payload.can_serve_stale(Instant::now()) {
251284
tracing::warn!(error = %err, "refresh failed, serving stale data");
252285

286+
#[cfg(feature = "metrics")]
253287
self.observe_hit(true);
254288

255289
return Ok(payload.jwks.clone());
@@ -261,8 +295,10 @@ impl CacheManager {
261295
self.refresh_blocking(true).await?
262296
{
263297
if from_cache {
298+
#[cfg(feature = "metrics")]
264299
self.observe_hit(false);
265300
} else {
301+
#[cfg(feature = "metrics")]
266302
self.observe_miss();
267303
}
268304
return Ok(jwks);
@@ -413,6 +449,7 @@ impl CacheManager {
413449
let request = request;
414450

415451
while let AttemptBudget::Granted { timeout } = executor.attempt_budget() {
452+
#[cfg(feature = "metrics")]
416453
let attempt_started = Instant::now();
417454
let fetch = fetch_jwks(&self.client, &self.registration, &request, timeout).await;
418455

@@ -463,6 +500,7 @@ impl CacheManager {
463500
let jwks = payload.jwks.clone();
464501

465502
self.commit_success(mode, payload).await;
503+
#[cfg(feature = "metrics")]
466504
self.observe_refresh_success(attempt_started.elapsed());
467505

468506
return Ok(RefreshOutcome::Updated { jwks, from_cache: false });
@@ -503,6 +541,7 @@ impl CacheManager {
503541
},
504542
}
505543

544+
#[cfg(feature = "metrics")]
506545
self.observe_refresh_error();
507546

508547
if !force_revalidation
@@ -569,6 +608,7 @@ impl CacheManager {
569608
}
570609
}
571610

611+
#[cfg(feature = "metrics")]
572612
fn observe_hit(&self, stale: bool) {
573613
let tenant = &self.registration.tenant_id;
574614
let provider = &self.registration.provider_id;
@@ -578,6 +618,7 @@ impl CacheManager {
578618
self.metrics.record_hit(stale);
579619
}
580620

621+
#[cfg(feature = "metrics")]
581622
fn observe_miss(&self) {
582623
let tenant = &self.registration.tenant_id;
583624
let provider = &self.registration.provider_id;
@@ -587,6 +628,7 @@ impl CacheManager {
587628
self.metrics.record_miss();
588629
}
589630

631+
#[cfg(feature = "metrics")]
590632
fn observe_refresh_success(&self, duration: Duration) {
591633
let tenant = &self.registration.tenant_id;
592634
let provider = &self.registration.provider_id;
@@ -596,6 +638,7 @@ impl CacheManager {
596638
self.metrics.record_refresh_success(duration);
597639
}
598640

641+
#[cfg(feature = "metrics")]
599642
fn observe_refresh_error(&self) {
600643
let tenant = &self.registration.tenant_id;
601644
let provider = &self.registration.provider_id;

src/error.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub enum Error {
4040
#[error("Validation failed for {field}: {reason}")]
4141
Validation { field: &'static str, reason: String },
4242
}
43+
#[cfg(feature = "metrics")]
4344
impl<T> From<metrics::SetRecorderError<T>> for Error
4445
where
4546
T: std::fmt::Display,

0 commit comments

Comments
 (0)