dialcache/observe.rs
1//! Backend-neutral diagnostics: metric events and structured log events.
2
3use std::fmt;
4use std::sync::Arc;
5
6use crate::error::BoxError;
7pub use crate::remote::MissReason;
8
9macro_rules! str_enum {
10 ($(#[$meta:meta])* $name:ident { $($(#[$vmeta:meta])* $variant:ident => $text:literal),+ $(,)? }) => {
11 $(#[$meta])*
12 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13 pub enum $name { $($(#[$vmeta])* $variant),+ }
14 impl $name {
15 /// The `snake_case` label value shared with the TypeScript and Go
16 /// ports and published by the bundled metric exporters.
17 pub fn as_str(self) -> &'static str {
18 match self { $($name::$variant => $text),+ }
19 }
20 }
21 impl fmt::Display for $name {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) }
23 }
24 };
25}
26
27str_enum! {
28 /// The cache layer a diagnostic refers to.
29 Layer {
30 /// No layer was reached: caching was disabled for the scope, key
31 /// construction failed, or the policy could not be resolved.
32 Noop => "noop",
33 /// The outermost enabled scope's request memo.
34 RequestLocal => "request_local",
35 /// The process-local store.
36 Local => "local",
37 /// The remote adapter on the caller-serving path.
38 Remote => "remote",
39 /// The remote adapter inside detached shadow work: its reads, fills,
40 /// serialization, compression and payload sizes.
41 RemoteShadow => "remote_shadow",
42 }
43}
44
45str_enum! {
46 /// Where an in-flight execution was shared.
47 CoalescingScope {
48 /// The flight was registered on the outermost enabled scope of the
49 /// same request.
50 RequestLocal => "request_local",
51 /// The flight was registered on the cache instance; separate
52 /// instances in one process do not share flights.
53 Process => "process",
54 }
55}
56
57str_enum! {
58 /// Why a cache layer was skipped.
59 DisabledReason {
60 /// Caching is not enabled for the call's scope; the source runs directly.
61 Context => "context",
62 /// The layer has no effective TTL after the runtime overlay, which
63 /// includes the default of an omitted policy.
64 PolicyDisabled => "policy_disabled",
65 /// The merged TTL leaf is not a whole number of seconds within 365
66 /// days; only this layer is skipped, with a `config_resolution` error.
67 InvalidTtl => "invalid_ttl",
68 /// The merged ramp leaf is not a finite number in `[0, 100]`; only
69 /// this layer is skipped, with a `config_resolution` error.
70 InvalidRamp => "invalid_ramp",
71 /// The layer is configured but this key's stable cohort sample falls
72 /// outside its ramp.
73 RampedDown => "ramped_down",
74 /// The whole invocation's policy could not be resolved (`noop`
75 /// layer), or the process-local read failed (`local` layer).
76 ConfigError => "config_error",
77 }
78}
79
80str_enum! {
81 /// Stable failure sites.
82 ErrorKind {
83 /// The key selector failed or panicked, or the identity could not
84 /// form a key.
85 KeyConstruction => "key_construction",
86 /// The policy provider or resolution failed for the invocation, a
87 /// layer leaf was invalid, or a recovery or shadow option was rejected.
88 ConfigResolution => "config_resolution",
89 /// A process-local read failed, or a remote read failed for a reason
90 /// other than its deadline.
91 CacheRead => "cache_read",
92 /// A remote read did not answer within its read budget.
93 CacheReadTimeout => "cache_read_timeout",
94 /// A process-local or remote write failed, or the write stamp left
95 /// the safe-integer domain.
96 CacheWrite => "cache_write",
97 /// A tracked write asked for retention above the one-hour physical
98 /// cap and was clamped: a configuration signal, not a failure.
99 TrackedTtlClamped => "tracked_ttl_clamped",
100 /// The codec failed or panicked while decoding a stored payload.
101 SerializationLoad => "serialization_load",
102 /// The codec failed or panicked while encoding a value for the remote layer.
103 SerializationDump => "serialization_dump",
104 /// zstd compression failed while preparing a remote write.
105 Compression => "compression",
106 /// An explicit invalidation failed, including one made without a
107 /// remote adapter.
108 Invalidation => "invalidation",
109 /// The source failed, panicked or exceeded its deadline; reported
110 /// with `in_fallback` set.
111 Fallback => "fallback",
112 }
113}
114
115str_enum! {
116 /// Terminal outcomes of sampled shadow validation.
117 ShadowOutcome {
118 /// The cached and source values compared equal.
119 Match => "match",
120 /// They differed, and a confirmation read found the stored payload
121 /// unchanged.
122 Mismatch => "mismatch",
123 /// They differed, but the stored payload changed or disappeared
124 /// before confirmation.
125 Superseded => "superseded",
126 /// A clean shadow miss was populated.
127 Filled => "filled",
128 /// A tracked fill was withheld because its stamp did not exceed the
129 /// watermark observed by the initial read.
130 FillFenced => "fill_fenced",
131 /// Preparing (encoding, compression) or writing a clean-miss fill failed.
132 FillError => "fill_error",
133 /// The initial detached remote read failed, including by deadline.
134 RedisError => "redis_error",
135 /// The source-of-truth read failed.
136 SourceError => "source_error",
137 /// The retained payload could not be decoded for comparison.
138 DeserializationError => "deserialization_error",
139 /// The comparator returned an error or panicked.
140 ComparisonError => "comparison_error",
141 /// The confirmation remote read failed.
142 ConfirmationError => "confirmation_error",
143 /// The shadow deadline expired before a verdict.
144 Timeout => "timeout",
145 /// Per-key deduplication or the instance's in-flight cap rejected
146 /// the job before it started.
147 Dropped => "dropped",
148 }
149}
150
151str_enum! {
152 /// Outcomes of one confirmed-mismatch warning attempt. When more than one
153 /// failure occurs, `hook_error` ranks above `render_error`, which ranks
154 /// above `logger_error`.
155 ShadowMismatchLoggingOutcome {
156 /// Every requested field was built and the logger returned.
157 Logged => "logged",
158 /// The operation's `mismatch_diff` hook or the built-in walk panicked
159 /// or failed; `mismatch_paths`, `mismatch_path_count` and `diff_json`
160 /// are unavailable.
161 HookError => "hook_error",
162 /// A compared value had no JSON rendering, the operation has no
163 /// renderer for a requested field, or the bounded CPU executor
164 /// rejected the warning's work; the fields it feeds are unavailable.
165 RenderError => "render_error",
166 /// The logger panicked while receiving the warning.
167 LoggerError => "logger_error",
168 }
169}
170
171str_enum! {
172 /// Terminal outcomes of stale-on-error recovery.
173 RecoveryOutcome {
174 /// A retained stale frame passed the return-time age check and
175 /// supplied the result.
176 Served => "served",
177 /// No retained frame remained eligible: none was kept, or its age
178 /// reached the ceiling.
179 Miss => "miss",
180 /// Decoding the retained frame failed.
181 DeserializationError => "deserialization_error",
182 }
183}
184
185str_enum! {
186 /// Compression outcomes for writes and reads.
187 CompressionOutcome {
188 /// Write: the payload met the threshold and the marked zstd frame was
189 /// smaller than the escaped raw form.
190 Compressed => "compressed",
191 /// Write: the payload was below the threshold and was stored raw.
192 BelowThreshold => "below_threshold",
193 /// Write: compression ran but did not shrink the payload; stored raw.
194 NotSmaller => "not_smaller",
195 /// Write: the payload exceeds the decompression cap and was stored raw
196 /// so a read could still accept it; a capacity signal.
197 WriteOverLimit => "write_over_limit",
198 /// Read: a marked zstd frame decoded successfully.
199 Decompressed => "decompressed",
200 /// Read: zstd rejected a marked payload as malformed or truncated; the
201 /// stored bytes were handed through untouched.
202 FallbackRaw => "fallback_raw",
203 /// Read: decompressing would exceed the cap; the stored bytes were
204 /// handed through untouched. An integrity signal.
205 ReadOverLimit => "read_over_limit",
206 }
207}
208
209str_enum! {
210 /// Which codec direction a serialization timing measures.
211 SerializationOperation {
212 /// Encoding a value for the remote layer.
213 Dump => "dump",
214 /// Decoding a stored payload.
215 Load => "load",
216 }
217}
218
219str_enum! {
220 /// Which envelope direction a compression timing measures.
221 CompressionOperation {
222 /// zstd compression during a write.
223 Compress => "compress",
224 /// zstd decompression during a read.
225 Decompress => "decompress",
226 }
227}
228
229/// Labels shared by layer-scoped events. Logical keys are never labels.
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct Labels {
232 /// The instance namespace (default `urn`); unrelated to any exporter's
233 /// metric-name namespace.
234 pub namespace: Arc<str>,
235 /// The operation name; `watermark` on invalidation error events.
236 pub use_case: Arc<str>,
237 /// The entity type of the identity.
238 pub key_type: Arc<str>,
239 /// The layer the event refers to, or [`Layer::Noop`] when none was reached.
240 pub layer: Layer,
241}
242
243/// Labels of outcome-scoped events (shadow validation and recovery).
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct OutcomeLabels {
246 /// The instance namespace (default `urn`); unrelated to any exporter's
247 /// metric-name namespace.
248 pub namespace: Arc<str>,
249 /// The operation name.
250 pub use_case: Arc<str>,
251 /// The entity type of the identity.
252 pub key_type: Arc<str>,
253}
254
255/// One public diagnostic. Counters carry no value; timers report seconds;
256/// sizes report bytes.
257#[derive(Debug, Clone, PartialEq)]
258pub enum Event {
259 /// One lookup of an enabled cache layer, hit or miss. Emitted before the
260 /// result for the remote layer and after it for the request-local and
261 /// process-local layers; a layer whose read failed emits `Error` and
262 /// `Disabled` instead.
263 Request {
264 /// The layer that was looked up.
265 labels: Labels,
266 },
267 /// A layer lookup found no servable value.
268 Miss {
269 /// The layer that missed.
270 labels: Labels,
271 /// The bounded cause; request-local and process-local misses are
272 /// always [`MissReason::ValueAbsent`].
273 reason: MissReason,
274 },
275 /// A layer, or the whole call (`noop`), was skipped.
276 Disabled {
277 /// The skipped layer, or [`Layer::Noop`] when no layer was reached.
278 labels: Labels,
279 /// Why it was skipped.
280 reason: DisabledReason,
281 },
282 /// A bounded failure site fired. Cache plumbing failures fail open; only
283 /// source failures reach the caller.
284 Error {
285 /// The layer whose work failed.
286 labels: Labels,
287 /// The failure site.
288 error: ErrorKind,
289 /// `true` when the source (fallback) failed or timed out rather than
290 /// cache plumbing.
291 in_fallback: bool,
292 },
293 /// One explicit invalidation attempt, emitted before the remote call and
294 /// even when no remote adapter is configured.
295 Invalidation {
296 /// The namespace whose entity watermark is advanced.
297 namespace: Arc<str>,
298 /// The entity type being invalidated.
299 key_type: Arc<str>,
300 /// Always [`Layer::Remote`]: watermarks live only in the remote layer.
301 layer: Layer,
302 },
303 /// A caller joined an execution already in flight for the same logical
304 /// key instead of starting its own.
305 Coalesced {
306 /// The identity of the joined execution.
307 labels: OutcomeLabels,
308 /// Whether the flight was registered on the request scope or the instance.
309 scope: CoalescingScope,
310 },
311 /// One terminal outcome of an admitted or explicitly dropped shadow job.
312 ShadowValidation {
313 /// The identity the job validated.
314 labels: OutcomeLabels,
315 /// The terminal verdict.
316 outcome: ShadowOutcome,
317 },
318 /// Age in seconds of the validated value at a match or confirmed mismatch, clamped at zero.
319 ShadowValueAge {
320 /// The identity the job validated.
321 labels: OutcomeLabels,
322 /// [`ShadowOutcome::Match`] or [`ShadowOutcome::Mismatch`]; no other
323 /// outcome carries an age.
324 outcome: ShadowOutcome,
325 /// Observing wall clock minus the frame's `created_at_ms`, never negative.
326 seconds: f64,
327 },
328 /// One confirmed-mismatch warning attempt, emitted after the warning and
329 /// only while mismatch logging is on.
330 ShadowMismatchLogging {
331 /// The identity the job validated.
332 labels: OutcomeLabels,
333 /// Whether the warning was built and delivered, or why not.
334 outcome: ShadowMismatchLoggingOutcome,
335 },
336 /// Positive offset in seconds of a frame dated after the observing clock.
337 FutureTimestampOffset {
338 /// The layer whose read decoded the frame.
339 labels: Labels,
340 /// How far the frame's `created_at_ms` lies ahead of the observing wall clock.
341 seconds: f64,
342 },
343 /// One classifier-authorized stale-on-error recovery check after a
344 /// source failure.
345 StaleRecovery {
346 /// The identity being recovered.
347 labels: OutcomeLabels,
348 /// Whether a retained frame was served.
349 outcome: RecoveryOutcome,
350 },
351 /// Age of the retained frame at the moment recovery served it; emitted
352 /// only alongside [`RecoveryOutcome::Served`].
353 StaleRecoveryValueAge {
354 /// The identity being recovered.
355 labels: OutcomeLabels,
356 /// Always [`RecoveryOutcome::Served`].
357 outcome: RecoveryOutcome,
358 /// Observing wall clock minus the frame's `created_at_ms`, clamped at zero.
359 seconds: f64,
360 },
361 /// One envelope decision: on every write while compression is enabled,
362 /// and on reads whose payload carried a zstd marker.
363 Compression {
364 /// The layer performing the write or read.
365 labels: Labels,
366 /// Write and read outcomes are disjoint sets; see [`CompressionOutcome`].
367 outcome: CompressionOutcome,
368 },
369 /// Lookup latency of one layer, hit or miss; a remote read's value
370 /// includes any wait up to its deadline.
371 Get {
372 /// The layer looked up.
373 labels: Labels,
374 /// Elapsed time of the lookup.
375 seconds: f64,
376 },
377 /// Time until the source settled or its deadline elapsed, once per
378 /// source invocation that served the caller (shadow re-validation runs
379 /// the source without it).
380 Fallback {
381 /// [`Layer::RequestLocal`] when the request memo was consulted,
382 /// [`Layer::Remote`] when the remote layer was consulted, otherwise
383 /// [`Layer::Local`] (even with the local layer disabled), and
384 /// [`Layer::Noop`] only when the call bypassed caching.
385 labels: Labels,
386 /// Elapsed time from invoking the source until it settled or its
387 /// deadline fired.
388 seconds: f64,
389 },
390 /// Codec latency of one encode or decode, emitted whether or not the
391 /// codec succeeded.
392 Serialization {
393 /// The layer the payload was written to or read from.
394 labels: Labels,
395 /// Encode (`dump`) or decode (`load`).
396 operation: SerializationOperation,
397 /// Elapsed time inside the codec.
398 seconds: f64,
399 },
400 /// Serializer output size of one remote write, before compression and escaping.
401 Size {
402 /// The layer being written.
403 labels: Labels,
404 /// Length of the codec output.
405 bytes: u64,
406 },
407 /// Prepared payload size of one remote write, after compression and
408 /// escaping, before dispatch.
409 StoredSize {
410 /// The layer being written.
411 labels: Labels,
412 /// Length of the payload handed to the adapter, excluding the
413 /// ten-byte frame header.
414 bytes: u64,
415 },
416 /// Size ratio of one write that chose the compressed form.
417 CompressionRatio {
418 /// The layer being written.
419 labels: Labels,
420 /// Marked compressed bytes divided by serializer output bytes.
421 ratio: f64,
422 },
423 /// zstd latency: on writes when compression ran (compressed or not
424 /// smaller), on reads when a marked frame was decoded or rejected.
425 CompressionDuration {
426 /// The layer performing the write or read.
427 labels: Labels,
428 /// Compress (write) or decompress (read).
429 operation: CompressionOperation,
430 /// Elapsed time of the envelope step.
431 seconds: f64,
432 },
433}
434
435/// Receives every public diagnostic. Failures (returned errors or panics)
436/// never change a cache, source or maintenance result.
437pub trait Observer: Send + Sync + 'static {
438 /// Receive one event. Called inline on the cache's own path under panic
439 /// isolation, so it should return quickly and never block.
440 fn observe(&self, event: &Event);
441
442 /// Whether this observer consumes shadow validation outcomes.
443 ///
444 /// Shadow validation is diagnostic work that exists only to be observed,
445 /// so the cache admits a shadow job only when the observer opts in by
446 /// returning `true`. The bundled metric exporters opt in.
447 fn observes_shadow_outcomes(&self) -> bool {
448 false
449 }
450}
451
452/// Bounded details of one confirmed shadow mismatch warning.
453///
454/// A field group is `None` when its leaf is off. Inside a requested group a
455/// `None` field is unavailable: [`logging_outcome`](Self::logging_outcome)
456/// names the failure, or, without one, the value was absent.
457#[derive(Debug, Clone, PartialEq)]
458pub struct ShadowMismatchDetails {
459 /// The instance namespace of the mismatching identity.
460 pub namespace: Arc<str>,
461 /// The operation name of the mismatching identity.
462 pub use_case: Arc<str>,
463 /// The entity type of the mismatching identity.
464 pub key_type: Arc<str>,
465 /// Age of the cached value at the verdict, as [`Event::ShadowValueAge`]
466 /// records it.
467 pub cached_value_age_seconds: f64,
468 /// The logical key, bounded to 2 KiB of UTF-8.
469 pub cache_key: String,
470 /// The difference fields; `None` when `diff` is off or construction failed.
471 pub diff: Option<ShadowMismatchDiffDetails>,
472 /// The JSON of both values; `None` when `value` is off or construction failed.
473 pub values: Option<ShadowMismatchValueDetails>,
474 /// Why requested fields are unavailable; `None` when every one was built.
475 /// Only [`ShadowMismatchLoggingOutcome::HookError`] and
476 /// [`ShadowMismatchLoggingOutcome::RenderError`] appear here.
477 pub logging_outcome: Option<ShadowMismatchLoggingOutcome>,
478}
479
480/// The difference fields of a confirmed-mismatch warning, present when
481/// `mismatch_logging.diff` is above [`MismatchDiff::Off`](crate::policy::MismatchDiff::Off).
482#[derive(Debug, Clone, PartialEq, Eq)]
483pub struct ShadowMismatchDiffDetails {
484 /// The effective level: [`Paths`](crate::policy::MismatchDiff::Paths) or
485 /// [`Values`](crate::policy::MismatchDiff::Values).
486 pub level: crate::policy::MismatchDiff,
487 /// RFC 6901 JSON Pointers of the differing fields in walk order, at most
488 /// 64, each bounded to 512 bytes; `None` when the hook or walk failed.
489 pub mismatch_paths: Option<Vec<String>>,
490 /// The number of differences before the cut to 64; `None` with `mismatch_paths`.
491 pub mismatch_path_count: Option<u64>,
492 /// At level `Values`, the entries as a JSON array of `{path, cached,
493 /// source}` objects, each side bounded to 1 KiB and the whole to 8 KiB;
494 /// `None` at level `Paths` and when unavailable.
495 pub diff_json: Option<String>,
496}
497
498/// The whole-value fields of a confirmed-mismatch warning, present when
499/// `mismatch_logging.value` is on.
500#[derive(Debug, Clone, PartialEq, Eq)]
501pub struct ShadowMismatchValueDetails {
502 /// JSON of the decoded cached value, bounded to 8 KiB; `None` when unavailable.
503 pub cached_value_json: Option<String>,
504 /// JSON of the source value, bounded to 8 KiB; `None` when unavailable.
505 pub source_value_json: Option<String>,
506}
507
508/// Severity of a [`LogEvent`].
509#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
510pub enum LogLevel {
511 /// Verbose diagnostics; no [`LogEvent`] maps here today.
512 Debug,
513 /// A diagnostic on a path where the cache itself added no failure: a
514 /// layer failed open and the call went on to another layer or the
515 /// source, or an explicit invalidation or a recovery decode failed and
516 /// the operation reports that failure.
517 Warn,
518 /// A whole path is broken: key construction or the process-local store.
519 Error,
520}
521
522/// Structured log events. All are fail-open diagnostics except the
523/// invalidation failure, which is also returned to the caller.
524#[derive(Debug)]
525pub enum LogEvent {
526 /// The key selector failed or panicked, or the identity could not form a
527 /// key; the source ran uncached. [`LogLevel::Error`].
528 KeyConstructionFailed(BoxError),
529 /// The policy provider failed or panicked, or the merged policy was
530 /// malformed; the source ran uncached.
531 PolicyResolutionFailed(BoxError),
532 /// The [`LocalStore`](crate::LocalStore) read failed or panicked; the
533 /// local layer was skipped for the call. [`LogLevel::Error`].
534 LocalReadFailed(BoxError),
535 /// The [`LocalStore`](crate::LocalStore) write failed or panicked; the
536 /// value was still returned.
537 LocalWriteFailed(BoxError),
538 /// The caller-serving remote read failed or exceeded its read budget.
539 /// Detached shadow reads do not log.
540 RemoteReadFailed(BoxError),
541 /// Preparing or dispatching a caller-serving remote write failed:
542 /// encoding, compression, the write stamp or the adapter.
543 RemoteWriteFailed(BoxError),
544 /// The stale-recovery classifier panicked; recovery was denied.
545 RecoveryPredicateFailed(BoxError),
546 /// Decoding the retained stale frame failed during recovery; the source
547 /// error was returned. Default formatting omits value-bearing error text;
548 /// custom loggers can inspect the original error here.
549 RecoveryDecodeFailed(BoxError),
550 /// A shadow job's clean-miss fill failed while preparing or writing the payload.
551 ShadowFillFailed(BoxError),
552 /// A confirmed shadow mismatch, logged only when the resolved policy's
553 /// mismatch-logging `value` is on or its `diff` is above `off`.
554 ShadowMismatch(ShadowMismatchDetails),
555 /// An explicit invalidation failed; the same error is returned to the caller.
556 InvalidationFailed(BoxError),
557}
558
559impl LogEvent {
560 /// [`LogLevel::Error`] for key construction and process-local read
561 /// failures, [`LogLevel::Warn`] for everything else.
562 pub fn level(&self) -> LogLevel {
563 match self {
564 LogEvent::KeyConstructionFailed(_) | LogEvent::LocalReadFailed(_) => LogLevel::Error,
565 _ => LogLevel::Warn,
566 }
567 }
568}
569
570impl fmt::Display for LogEvent {
571 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
572 match self {
573 LogEvent::KeyConstructionFailed(e) => {
574 write!(f, "Could not construct DialCache key: {e}")
575 }
576 LogEvent::PolicyResolutionFailed(e) => {
577 write!(f, "Could not resolve DialCache key config: {e}")
578 }
579 LogEvent::LocalReadFailed(e) => write!(f, "Error getting value from local cache: {e}"),
580 LogEvent::LocalWriteFailed(e) => write!(f, "Error putting value in local cache: {e}"),
581 LogEvent::RemoteReadFailed(e) => write!(f, "Error getting value from Redis cache: {e}"),
582 LogEvent::RemoteWriteFailed(e) => write!(f, "Error putting value in Redis cache: {e}"),
583 LogEvent::RecoveryPredicateFailed(e) => {
584 write!(
585 f,
586 "DialCache stale recovery predicate failed; recovery was denied: {e}"
587 )
588 }
589 LogEvent::RecoveryDecodeFailed(e) => {
590 write!(
591 f,
592 "Error using retained Redis value during stale recovery: "
593 )?;
594 if let Some(json) = e.downcast_ref::<serde_json::Error>() {
595 write!(
596 f,
597 "JSON {:?} at line {} column {}",
598 json.classify(),
599 json.line(),
600 json.column()
601 )
602 } else {
603 write!(f, "decoding failed")
604 }
605 }
606 LogEvent::ShadowFillFailed(e) => {
607 write!(f, "Error populating Redis from DialCache shadow work: {e}")
608 }
609 LogEvent::ShadowMismatch(d) => {
610 write!(f, "DialCache shadow validation mismatch: namespace={} useCase={} keyType={} cachedValueAgeSeconds={} cacheKey={}",
611 d.namespace, d.use_case, d.key_type, d.cached_value_age_seconds, d.cache_key)?;
612 // Unavailable fields are left out; loggingOutcome says why.
613 if let Some(diff) = &d.diff {
614 if let (Some(paths), Some(count)) =
615 (&diff.mismatch_paths, diff.mismatch_path_count)
616 {
617 let paths = serde_json::to_string(paths).map_err(|_| fmt::Error)?;
618 write!(f, " mismatchPaths={paths} mismatchPathCount={count}")?;
619 }
620 if let Some(json) = &diff.diff_json {
621 write!(f, " diffJson={json}")?;
622 }
623 }
624 if let Some(values) = &d.values {
625 if let Some(value) = &values.cached_value_json {
626 write!(f, " cachedValue={value}")?;
627 }
628 if let Some(value) = &values.source_value_json {
629 write!(f, " sourceValue={value}")?;
630 }
631 }
632 if let Some(outcome) = d.logging_outcome {
633 write!(f, " loggingOutcome={outcome}")?;
634 }
635 Ok(())
636 }
637 LogEvent::InvalidationFailed(e) => {
638 write!(f, "Error writing DialCache invalidation watermark: {e}")
639 }
640 }
641 }
642}
643
644/// Receives structured log events. Panics are isolated from cache results.
645pub trait Logger: Send + Sync + 'static {
646 /// Receive one event. Called inline under panic isolation; a panic here
647 /// is swallowed and the cache result is unaffected.
648 fn log(&self, event: &LogEvent);
649}
650
651/// The default logger: the `log` crate facade.
652#[derive(Debug, Clone, Copy, Default)]
653pub struct LogFacadeLogger;
654
655impl Logger for LogFacadeLogger {
656 fn log(&self, event: &LogEvent) {
657 match event.level() {
658 LogLevel::Debug => log::debug!(target: "dialcache", "{event}"),
659 LogLevel::Warn => log::warn!(target: "dialcache", "{event}"),
660 LogLevel::Error => log::error!(target: "dialcache", "{event}"),
661 }
662 }
663}