use std::time::Duration;
#[cfg(any(feature = "logs", test))]
use thread_aware::{Arc, PerCore};
use crate::cache::CacheName;
use crate::telemetry::attributes;
#[cfg(any(feature = "logs", test))]
#[derive(Clone, Debug)]
pub(crate) struct CacheTelemetryInner {
pub(crate) logging_enabled: bool,
}
#[cfg(any(feature = "logs", test))]
impl CacheTelemetryInner {
#[inline]
fn debug(&self, cache_name: CacheName, event: &'static str, duration: Duration) {
if self.logging_enabled {
tracing::debug!(
cache.name = cache_name,
cache.event = event,
cache.duration_ns = duration.as_nanos()
);
}
}
#[inline]
fn info(&self, cache_name: CacheName, event: &'static str, duration: Duration) {
if self.logging_enabled {
tracing::info!(
cache.name = cache_name,
cache.event = event,
cache.duration_ns = duration.as_nanos()
);
}
}
#[inline]
fn error(&self, cache_name: CacheName, event: &'static str, duration: Duration) {
if self.logging_enabled {
tracing::error!(
cache.name = cache_name,
cache.event = event,
cache.duration_ns = duration.as_nanos()
);
}
}
}
#[cfg(not(any(feature = "logs", test)))]
#[derive(Clone, Debug, Default)]
pub(crate) struct CacheTelemetryInner;
#[cfg(not(any(feature = "logs", test)))]
#[expect(clippy::unused_self, reason = "Methods must match the logs-enabled impl signature")]
impl CacheTelemetryInner {
#[inline]
fn debug(&self, _: CacheName, _: &'static str, _: Duration) {}
#[inline]
fn info(&self, _: CacheName, _: &'static str, _: Duration) {}
#[inline]
fn error(&self, _: CacheName, _: &'static str, _: Duration) {}
}
#[derive(Clone, Debug)]
pub struct CacheTelemetry {
#[cfg(any(feature = "logs", test))]
pub(crate) inner: Arc<CacheTelemetryInner, PerCore>,
#[cfg(not(any(feature = "logs", test)))]
pub(crate) inner: CacheTelemetryInner,
}
impl CacheTelemetry {
#[must_use]
pub(crate) fn new() -> Self {
#[cfg(any(feature = "logs", test))]
{
Self {
inner: Arc::from_unaware(CacheTelemetryInner { logging_enabled: false }),
}
}
#[cfg(not(any(feature = "logs", test)))]
{
Self {
inner: CacheTelemetryInner,
}
}
}
#[cfg(any(feature = "logs", test))]
#[must_use]
pub(crate) fn with_logging() -> Self {
Self {
inner: Arc::from_unaware(CacheTelemetryInner { logging_enabled: true }),
}
}
#[inline]
pub(crate) fn cache_hit(&self, cache_name: CacheName, duration: Duration) {
self.inner.debug(cache_name, attributes::EVENT_HIT, duration);
}
#[inline]
pub(crate) fn cache_miss(&self, cache_name: CacheName, duration: Duration) {
self.inner.debug(cache_name, attributes::EVENT_MISS, duration);
}
#[inline]
pub(crate) fn cache_expired(&self, cache_name: CacheName, duration: Duration) {
self.inner.info(cache_name, attributes::EVENT_EXPIRED, duration);
}
#[inline]
pub(crate) fn get_error(&self, cache_name: CacheName, duration: Duration) {
self.inner.error(cache_name, attributes::EVENT_GET_ERROR, duration);
}
#[inline]
pub(crate) fn cache_fallback(&self, cache_name: CacheName, duration: Duration) {
self.inner.info(cache_name, attributes::EVENT_FALLBACK, duration);
}
#[inline]
pub(crate) fn refresh_hit(&self, cache_name: CacheName, duration: Duration) {
self.inner.debug(cache_name, attributes::EVENT_REFRESH_HIT, duration);
}
#[inline]
pub(crate) fn refresh_miss(&self, cache_name: CacheName, duration: Duration) {
self.inner.info(cache_name, attributes::EVENT_REFRESH_MISS, duration);
}
#[inline]
pub(crate) fn cache_inserted(&self, cache_name: CacheName, duration: Duration) {
self.inner.info(cache_name, attributes::EVENT_INSERTED, duration);
}
#[cfg(any(feature = "memory", test))]
#[inline]
pub(crate) fn cache_eviction(&self, cache_name: CacheName, duration: Duration) {
self.inner.info(cache_name, attributes::EVENT_EVICTION, duration);
}
#[inline]
pub(crate) fn insert_rejected(&self, cache_name: CacheName, duration: Duration) {
self.inner.info(cache_name, attributes::EVENT_INSERT_REJECTED, duration);
}
#[inline]
pub(crate) fn insert_error(&self, cache_name: CacheName, duration: Duration) {
self.inner.error(cache_name, attributes::EVENT_INSERT_ERROR, duration);
}
#[inline]
pub(crate) fn cache_invalidated(&self, cache_name: CacheName, duration: Duration) {
self.inner.info(cache_name, attributes::EVENT_INVALIDATED, duration);
}
#[inline]
pub(crate) fn invalidate_error(&self, cache_name: CacheName, duration: Duration) {
self.inner.error(cache_name, attributes::EVENT_INVALIDATE_ERROR, duration);
}
#[inline]
pub(crate) fn cache_cleared(&self, cache_name: CacheName, duration: Duration) {
self.inner.debug(cache_name, attributes::EVENT_CLEARED, duration);
}
#[inline]
pub(crate) fn clear_error(&self, cache_name: CacheName, duration: Duration) {
self.inner.error(cache_name, attributes::EVENT_CLEAR_ERROR, duration);
}
}
#[cfg(test)]
mod tests {
use testing_aids::LogCapture;
use super::*;
#[cfg_attr(miri, ignore)]
#[test]
fn logs_emit_contains_all_fields_and_values() {
let capture = LogCapture::new();
let _guard = tracing::subscriber::set_default(capture.subscriber());
let telemetry = CacheTelemetry::with_logging();
telemetry.invalidate_error("my_test_cache", Duration::from_nanos(12345));
capture.assert_contains(attributes::FIELD_NAME);
capture.assert_contains(attributes::FIELD_EVENT);
capture.assert_contains(attributes::FIELD_DURATION_NS);
capture.assert_contains("my_test_cache");
capture.assert_contains(attributes::EVENT_INVALIDATE_ERROR);
capture.assert_contains("12345");
}
#[cfg_attr(miri, ignore)]
#[test]
fn logs_emit_at_correct_severity_levels() {
let telemetry = CacheTelemetry::with_logging();
let capture = LogCapture::new();
let _guard = tracing::subscriber::set_default(capture.subscriber());
telemetry.get_error("cache", Duration::ZERO);
capture.assert_contains("ERROR");
let capture = LogCapture::new();
let _guard = tracing::subscriber::set_default(capture.subscriber());
telemetry.cache_expired("cache", Duration::ZERO);
capture.assert_contains("INFO");
let capture = LogCapture::new();
let _guard = tracing::subscriber::set_default(capture.subscriber());
telemetry.cache_hit("cache", Duration::ZERO);
capture.assert_contains("DEBUG");
}
#[cfg_attr(miri, ignore)]
#[test]
fn telemetry_disabled_emits_nothing() {
let telemetry = CacheTelemetry::new();
let capture = LogCapture::new();
let _guard = tracing::subscriber::set_default(capture.subscriber());
telemetry.cache_hit("cache", Duration::from_secs(1));
assert!(capture.output().is_empty());
}
#[cfg_attr(miri, ignore)]
fn assert_emits(f: impl FnOnce(&CacheTelemetry), expected: &str) {
let capture = LogCapture::new();
let _guard = tracing::subscriber::set_default(capture.subscriber());
let telemetry = CacheTelemetry::with_logging();
f(&telemetry);
capture.assert_contains(expected);
}
#[cfg_attr(miri, ignore)]
#[test]
fn every_helper_emits_its_event() {
assert_emits(|t| t.cache_hit("c", Duration::ZERO), attributes::EVENT_HIT);
assert_emits(|t| t.cache_miss("c", Duration::ZERO), attributes::EVENT_MISS);
assert_emits(|t| t.cache_expired("c", Duration::ZERO), attributes::EVENT_EXPIRED);
assert_emits(|t| t.get_error("c", Duration::ZERO), attributes::EVENT_GET_ERROR);
assert_emits(|t| t.cache_fallback("c", Duration::ZERO), attributes::EVENT_FALLBACK);
assert_emits(|t| t.refresh_hit("c", Duration::ZERO), attributes::EVENT_REFRESH_HIT);
assert_emits(|t| t.refresh_miss("c", Duration::ZERO), attributes::EVENT_REFRESH_MISS);
assert_emits(|t| t.cache_inserted("c", Duration::ZERO), attributes::EVENT_INSERTED);
assert_emits(|t| t.cache_eviction("c", Duration::ZERO), attributes::EVENT_EVICTION);
assert_emits(|t| t.insert_rejected("c", Duration::ZERO), attributes::EVENT_INSERT_REJECTED);
assert_emits(|t| t.insert_error("c", Duration::ZERO), attributes::EVENT_INSERT_ERROR);
assert_emits(|t| t.cache_invalidated("c", Duration::ZERO), attributes::EVENT_INVALIDATED);
assert_emits(|t| t.invalidate_error("c", Duration::ZERO), attributes::EVENT_INVALIDATE_ERROR);
assert_emits(|t| t.cache_cleared("c", Duration::ZERO), attributes::EVENT_CLEARED);
assert_emits(|t| t.clear_error("c", Duration::ZERO), attributes::EVENT_CLEAR_ERROR);
}
}