Skip to main content

cedarling/
lib.rs

1// This software is available under the Apache-2.0 license.
2// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text.
3//
4// Copyright (c) 2024, Gluu, Inc.
5
6#![deny(missing_docs)]
7#![warn(unreachable_pub)]
8#![allow(clippy::missing_errors_doc)]
9//! # Cedarling
10//! The Cedarling is a performant local authorization service that runs the Rust Cedar Engine.
11//! Cedar policies and schema are loaded at startup from a locally cached "Policy Store".
12//! In simple terms, the Cedarling returns the answer: should the application allow this action on this resource given these JWT tokens.
13//! "Fit for purpose" policies help developers build a better user experience.
14//! For example, why display form fields that a user is not authorized to see?
15//! The Cedarling is a more productive and flexible way to handle authorization.
16
17mod async_sleep;
18mod authz;
19mod bootstrap_config;
20mod common;
21mod context_data_api;
22mod entity_builder;
23mod http;
24mod http_utils;
25mod init;
26mod jwt;
27mod lock;
28mod log;
29// is reexported in hidden bindings module
30#[doc(hidden)]
31pub mod sparkv;
32
33#[cfg(not(target_arch = "wasm32"))]
34#[cfg(feature = "blocking")]
35pub mod blocking;
36
37#[doc(hidden)]
38#[cfg(test)]
39mod tests;
40
41use std::collections::{HashMap, HashSet};
42use std::{fmt::Write, sync::Arc};
43
44use crate::authz::metrics::MetricsCollector;
45use crate::context_data_api::DataStore;
46pub use crate::context_data_api::{
47    CedarType, CedarValueMapper, ConfigValidationError, DataApi, DataEntry, DataError,
48    DataStoreConfig, DataStoreStats, DataValidator, ExtensionValue, ValidationConfig,
49    ValidationError, ValidationResult, ValueMappingError,
50};
51pub use crate::jwt::TrustedIssuerLoadingInfo;
52use authz::Authz;
53pub use authz::request::{
54    AuthorizeMultiIssuerRequest, BatchAuthorizeMultiIssuerRequest, BatchAuthorizeResponse,
55    BatchAuthorizeUnsignedRequest, BatchItem, CedarEntityMapping, EntityData, RequestUnsigned,
56    TokenInput,
57};
58pub use authz::{
59    AuthorizeError, AuthorizeResult, BatchItemError, MultiIssuerAuthorizeResult,
60};
61pub use bootstrap_config::*;
62/// Identifier of a Cedar policy, re-exported from [`cedar_policy`] so callers can
63/// pass the policy IDs from `response.diagnostics().reason()` to the annotation
64/// lookup methods without depending on `cedar_policy` directly.
65pub use cedar_policy::PolicyId;
66use common::app_types::{self, ApplicationName};
67pub use common::policy_store::{PolicyEffect, PolicyMetadata};
68pub use http::HttpClientConfig;
69use init::ServiceFactory;
70use init::policy_store::{LoadedPolicyStore, load_policy_store};
71use init::policy_store_refresh::{
72    AuthzRebuilder, PolicyStoreRefreshHandle, RefreshSource, RefreshWorkerSeed, WorkerContext,
73    spawn_refresh_worker,
74};
75use init::service_config::{ServiceConfig, ServiceConfigError};
76use init::service_factory::ServiceInitError;
77use lock::InitLockServiceError;
78use lock::health_registry::HealthStatus;
79use log::interface::LogWriter;
80use log::{BaseLogEntry, LogEntry};
81pub use log::{LogLevel, LogStorage};
82
83use semver::Version;
84
85/// Git commit hash at build time (`None` if git is unavailable or
86/// `CEDARLING_BUILD_COMMIT` was not set at compile time).
87const BUILD_COMMIT: Option<&str> = option_env!("CEDARLING_BUILD_COMMIT");
88/// Build timestamp in RFC 3339 format (`None` if
89/// `CEDARLING_BUILD_TIMESTAMP` was not set at compile time).
90const BUILD_TIMESTAMP: Option<&str> = option_env!("CEDARLING_BUILD_TIMESTAMP");
91
92#[doc(hidden)]
93pub mod bindings {
94    pub use cedar_policy;
95
96    pub use super::log::{
97        AuthorizationLogInfo, Decision, Diagnostics, LogEntry, PolicyEvaluationError,
98    };
99    pub use crate::http::spawn_task;
100    pub use crate::sparkv;
101    pub use serde_json;
102    pub use serde_yaml_ng;
103}
104
105/// Errors that can occur during initialization Cedarling.
106#[derive(Debug, thiserror::Error)]
107pub enum InitCedarlingError {
108    /// Error while preparing config for internal services
109    #[error(transparent)]
110    ServiceConfig(#[from] ServiceConfigError),
111    /// Error while initializing a Service
112    #[error(transparent)]
113    ServiceInit(#[from] ServiceInitError),
114    /// Error while parse [`BootstrapConfigRaw`]
115    #[error(transparent)]
116    BootstrapConfigLoading(#[from] BootstrapConfigLoadingError),
117    /// Error while initializing the `DataStore` (invalid configuration)
118    #[error(transparent)]
119    DataStoreInit(#[from] ConfigValidationError),
120    #[cfg(feature = "blocking")]
121    /// Error while init tokio runtime
122    #[error(transparent)]
123    RuntimeInit(std::io::Error),
124    /// Error returned when Cedarling fails to obtain client credentials for sending
125    /// logs to the Lock Server.
126    #[error("failed to initialize the Lock Service: {0}")]
127    InitLockService(#[from] InitLockServiceError),
128}
129
130/// The instance of the Cedarling application.
131/// It is safe to share between threads.
132#[derive(Clone)]
133pub struct Cedarling {
134    log: log::Logger,
135    /// Wrapped in [`ArcSwap`] so the policy-store refresh worker can publish a
136    /// freshly built [`Authz`] (with new policy store, rebuilt JWT service and
137    /// entity builder) atomically. Every public method snapshots via
138    /// [`ArcSwap::load`] so an in-flight authorization keeps using the
139    /// pre-swap instance.
140    authz: Arc<arc_swap::ArcSwap<Authz>>,
141    data: Arc<DataStore>,
142    /// Held purely for its `Drop` side effect: dropping the last `Arc` closes
143    /// the worker's `oneshot` shutdown channel so the background refresh loop
144    /// exits when [`Cedarling`] goes away. The leading `_` tells the compiler
145    /// the field is intentionally not read.
146    _refresh_handle: Option<Arc<PolicyStoreRefreshHandle>>,
147}
148
149impl Cedarling {
150    /// Create a new instance of the Cedarling application.
151    /// Initialize instance from enviroment variables and from config.
152    /// Configuration structure has lower priority.
153    #[cfg(not(target_arch = "wasm32"))]
154    pub async fn new_with_env(
155        raw_config: Option<BootstrapConfigRaw>,
156    ) -> Result<Cedarling, InitCedarlingError> {
157        let config = BootstrapConfig::from_raw_config_and_env(raw_config)?;
158        Self::new(&config).await
159    }
160
161    /// Create a new instance of the Cedarling application.
162    pub async fn new(config: &BootstrapConfig) -> Result<Cedarling, InitCedarlingError> {
163        let pdp_id = app_types::PdpID::new();
164        let app_name = (!config.application_name.is_empty())
165            .then(|| ApplicationName::from(config.application_name.clone()));
166
167        let metrics = Arc::new(
168            if config
169                .lock_config
170                .as_ref()
171                .is_some_and(|c| c.telemetry_interval.is_some())
172            {
173                MetricsCollector::new(0)
174            } else {
175                MetricsCollector::disabled()
176            },
177        );
178
179        let log = crate::log::init_logger(
180            &config.log_config,
181            pdp_id,
182            app_name,
183            config.lock_config.as_ref(),
184            metrics.clone(),
185            config.http_client_config,
186        )
187        .await?;
188
189        log.log_any(
190            LogEntry::new(BaseLogEntry::new_system_opt_request_id(
191                LogLevel::INFO,
192                None,
193            ))
194            .set_message("Cedarling initialization started".to_string())
195            .set_build_info(BUILD_COMMIT, BUILD_TIMESTAMP),
196        );
197
198        // Bootstrap-load: build HttpClient + load policy store. Returns the
199        // service config plus the refresh-worker seed in one shot so the
200        // seed values (initial body hash, initial cache validators) stay in
201        // lexical scope right next to the refresh-worker spawn below.
202        let (service_config, refresh_seed) = perform_bootstrap_load(config, &log).await?;
203
204        let policy_count = service_config
205            .policy_store
206            .policies
207            .get_set()
208            .num_of_policies();
209        metrics.set_policy_count(policy_count);
210
211        // Initialize data store first so it can be passed to authz service
212        let data = Arc::new(DataStore::new(
213            config.data_store_config.clone(),
214            metrics.clone(),
215        )?);
216
217        let mut service_factory = ServiceFactory::new(
218            config,
219            service_config,
220            log.clone(),
221            data.clone(),
222            metrics.clone(),
223        );
224
225        // Log policy store metadata if available (new format only)
226        if let Some(metadata) = service_factory.policy_store_metadata() {
227            log_policy_store_metadata(&log, metadata);
228        }
229
230        if let Some(registry) = log.health_registry() {
231            registry.register("core", || HealthStatus::Success);
232            registry.register("policy_load", move || {
233                if policy_count > 0 {
234                    HealthStatus::Success
235                } else {
236                    HealthStatus::Failure
237                }
238            });
239        }
240
241        let authz = service_factory.authz_service().await?;
242        let authz_swap = Arc::new(arc_swap::ArcSwap::from(authz));
243
244        let refresh_handle = maybe_spawn_refresh_worker(
245            config,
246            &service_factory,
247            authz_swap.clone(),
248            log.clone(),
249            data.clone(),
250            metrics.clone(),
251            refresh_seed,
252        );
253
254        Ok(Cedarling {
255            log,
256            authz: authz_swap,
257            data,
258            _refresh_handle: refresh_handle,
259        })
260    }
261
262    // The following public methods retain async signatures for API compatibility
263    // to avoid breaking changes. They use #[allow(clippy::unused_async)] since
264    // they no longer await internally. Future maintainers can safely remove
265    // or refactor these methods when compatibility constraints allow.
266
267    /// Authorize request with unsigned data.
268    /// makes authorization decision based on the [`RequestUnverified`]
269    #[allow(clippy::unused_async)]
270    pub async fn authorize_unsigned(
271        &self,
272        request: RequestUnsigned,
273    ) -> Result<AuthorizeResult, AuthorizeError> {
274        self.authz.load().authorize_unsigned(&request)
275    }
276
277    /// Authorize a batch of unsigned requests against one shared principal.
278    ///
279    /// Runs setup work (principal build + pushed-data snapshot) once and
280    /// evaluates each item with its own resource and context. Results are
281    /// returned in input order, wrapped in a [`BatchAuthorizeResponse`] that
282    /// carries a shared `batch_id` for audit correlation.
283    ///
284    /// Batch-level failures (validation, principal parse) return `Err(AuthorizeError)`;
285    /// per-item failures are returned as `Err(BatchItemError)` for that item,
286    /// while genuine Cedar denials remain `Ok(AuthorizeResult)` with `decision=false`.
287    #[allow(clippy::unused_async)]
288    pub async fn authorize_unsigned_batch(
289        &self,
290        request: BatchAuthorizeUnsignedRequest,
291    ) -> Result<BatchAuthorizeResponse<Result<AuthorizeResult, BatchItemError>>, AuthorizeError>
292    {
293        self.authz.load().authorize_unsigned_batch(&request)
294    }
295
296    /// Authorize multi-issuer request.
297    /// makes authorization decision based on multiple JWT tokens from different issuers
298    #[allow(clippy::unused_async)]
299    pub async fn authorize_multi_issuer(
300        &self,
301        request: AuthorizeMultiIssuerRequest,
302    ) -> Result<MultiIssuerAuthorizeResult, AuthorizeError> {
303        self.authz.load().authorize_multi_issuer(&request)
304    }
305
306    /// Authorize a batch of multi-issuer requests against one shared token set.
307    ///
308    /// Validates tokens and builds token/issuer entities once, then evaluates
309    /// each item with its own resource and context. Results are returned in
310    /// input order, wrapped in a [`BatchAuthorizeResponse`] carrying a shared
311    /// `batch_id`. Batch-level failures (validation, JWT verification,
312    /// status-list refresh) return `Err(AuthorizeError)`; per-item failures are
313    /// returned as `Err(BatchItemError)`, while genuine Cedar denials remain 
314    /// `Ok(MultiIssuerAuthorizeResult)` with `decision=false`.
315    #[allow(clippy::unused_async)]
316    pub async fn authorize_multi_issuer_batch(
317        &self,
318        request: BatchAuthorizeMultiIssuerRequest,
319    ) -> Result<
320        BatchAuthorizeResponse<Result<MultiIssuerAuthorizeResult, BatchItemError>>,
321        AuthorizeError,
322    > {
323        self.authz.load().authorize_multi_issuer_batch(&request)
324    }
325
326    /// Returns metadata for all policies whose scope constraints are compatible
327    /// with the given principals, actions, and resources.
328    ///
329    /// This performs scope-level filtering only (principal/action/resource constraints).
330    /// Policies with `when`/`unless` conditions may still not apply at evaluation time.
331    pub fn get_matching_policies_unsigned(
332        &self,
333        principal: Option<&EntityData>,
334        actions: &[String],
335        resources: &[EntityData],
336    ) -> Result<Vec<PolicyMetadata>, AuthorizeError> {
337        self.authz
338            .load()
339            .get_matching_policies_unsigned(principal, actions, resources)
340    }
341
342    /// Returns metadata for all policies whose scope constraints are compatible
343    /// with the given token-derived principals, actions, and resources.
344    ///
345    /// Tokens are validated and their mapping types used as principal entity types.
346    pub fn get_matching_policies_multi_issuer(
347        &self,
348        tokens: &[TokenInput],
349        actions: &[String],
350        resources: &[EntityData],
351    ) -> Result<Vec<PolicyMetadata>, AuthorizeError> {
352        self.authz
353            .load()
354            .get_matching_policies_multi_issuer(tokens, actions, resources)
355    }
356
357    /// Merge the annotations (`@key("value")`) of the given policies into a single map.
358    ///
359    /// Intended for resolving the determining policies of an authorization
360    /// decision: pass the IDs from `result.response.diagnostics().reason()`.
361    ///
362    /// Lossy: if the same annotation key appears on several policies, one value
363    /// wins arbitrarily (order undefined). Use [`Self::annotation_values`] or
364    /// [`Self::annotations_by_policy`] when duplicates matter.
365    ///
366    /// Resolve annotations promptly after `authorize*()`: a concurrent policy-store
367    /// refresh may swap the store, in which case IDs that no longer resolve are
368    /// silently dropped from the result.
369    pub fn annotations_map<'a>(
370        &self,
371        ids: impl IntoIterator<Item = &'a PolicyId>,
372    ) -> HashMap<String, String> {
373        self.authz.load().annotations_map(ids)
374    }
375
376    /// Collect every value of the annotation `key` across the given policies,
377    /// preserving duplicates.
378    ///
379    /// Intended for resolving the determining policies of an authorization
380    /// decision: pass the IDs from `result.response.diagnostics().reason()`.
381    ///
382    /// Resolve annotations promptly after `authorize*()`: a concurrent policy-store
383    /// refresh may swap the store, in which case IDs that no longer resolve are
384    /// silently dropped from the result.
385    pub fn annotation_values<'a>(
386        &self,
387        ids: impl IntoIterator<Item = &'a PolicyId>,
388        key: &str,
389    ) -> Vec<String> {
390        self.authz.load().annotation_values(ids, key)
391    }
392
393    /// Return the annotations of each given policy, grouped by policy ID —
394    /// the loss-free companion to [`Self::annotations_map`].
395    ///
396    /// Intended for resolving the determining policies of an authorization
397    /// decision: pass the IDs from `result.response.diagnostics().reason()`.
398    ///
399    /// Resolve annotations promptly after `authorize*()`: a concurrent policy-store
400    /// refresh may swap the store, in which case IDs that no longer resolve are
401    /// silently dropped from the result.
402    pub fn annotations_by_policy<'a>(
403        &self,
404        ids: impl IntoIterator<Item = &'a PolicyId>,
405    ) -> HashMap<String, HashMap<String, String>> {
406        self.authz.load().annotations_by_policy(ids)
407    }
408
409    /// Closes the connections to the Lock Server and pushes all available logs.
410    pub async fn shut_down(&self) {
411        self.log.shut_down().await;
412    }
413}
414
415impl TrustedIssuerLoadingInfo for Cedarling {
416    fn is_trusted_issuer_loaded_by_name(&self, issuer_id: &str) -> bool {
417        self.authz
418            .load()
419            .is_trusted_issuer_loaded_by_name(issuer_id)
420    }
421
422    fn is_trusted_issuer_loaded_by_iss(&self, iss_claim: &str) -> bool {
423        self.authz.load().is_trusted_issuer_loaded_by_iss(iss_claim)
424    }
425
426    fn total_issuers(&self) -> usize {
427        self.authz.load().total_issuers()
428    }
429
430    fn loaded_trusted_issuers_count(&self) -> usize {
431        self.authz.load().loaded_trusted_issuers_count()
432    }
433
434    fn loaded_trusted_issuer_ids(&self) -> HashSet<String> {
435        self.authz.load().loaded_trusted_issuer_ids()
436    }
437
438    fn failed_trusted_issuer_ids(&self) -> HashSet<String> {
439        self.authz.load().failed_trusted_issuer_ids()
440    }
441}
442
443/// Build the HTTP client and load the policy store from the bootstrap config.
444/// Returns the parts the rest of `Cedarling::new` needs: the [`ServiceConfig`]
445/// for service-factory construction, and the [`RefreshWorkerSeed`] for the
446/// refresh worker's first-tick short-circuit. Wraps both fallible steps so
447/// the existing "configuration parsed successfully" / "...with error" log
448/// behavior covers the whole bootstrap unit.
449async fn perform_bootstrap_load(
450    config: &BootstrapConfig,
451    log: &log::Logger,
452) -> Result<(ServiceConfig, RefreshWorkerSeed), ServiceConfigError> {
453    let raw_load: Result<(http::HttpClient, LoadedPolicyStore), ServiceConfigError> = async {
454        let http_client = http::HttpClient::new(config.http_client_config)?;
455        let loaded = load_policy_store(
456            &config.policy_store_config,
457            &http_client,
458            config.authorization_config.strict_schema_validation,
459        )
460        .await?;
461        Ok((http_client, loaded))
462    }
463    .await;
464
465    let (http_client, loaded) = raw_load
466        .inspect(|_| {
467            log.log_any(
468                LogEntry::new(BaseLogEntry::new_system_opt_request_id(
469                    LogLevel::DEBUG,
470                    None,
471                ))
472                .set_message("configuration parsed successfully".to_string()),
473            );
474        })
475        .inspect_err(|err| {
476            log.log_any(
477                LogEntry::new(BaseLogEntry::new_system_opt_request_id(
478                    LogLevel::ERROR,
479                    None,
480                ))
481                .set_error(err.to_string())
482                .set_message("configuration parsed with error".to_string()),
483            );
484        })?;
485
486    let LoadedPolicyStore {
487        store: policy_store,
488        body_hash,
489        validators,
490    } = loaded;
491    Ok((
492        ServiceConfig {
493            policy_store,
494            http_client,
495        },
496        RefreshWorkerSeed {
497            initial_body_hash: body_hash,
498            initial_validators: validators,
499        },
500    ))
501}
502
503/// Spawn the background policy-store refresh worker if the source is a remote
504/// URL and a non-zero refresh interval was configured. Returns `None` for
505/// local sources or when refresh is disabled. The `seed` carries the
506/// `body_hash` and `validators` captured during initial bootstrap so the
507/// first periodic tick can short-circuit — passed in directly from the
508/// bootstrap-load result rather than detoured through `ServiceFactory`.
509fn maybe_spawn_refresh_worker(
510    config: &BootstrapConfig,
511    service_factory: &ServiceFactory<'_>,
512    authz_swap: Arc<arc_swap::ArcSwap<authz::Authz>>,
513    log: log::Logger,
514    data: Arc<context_data_api::DataStore>,
515    metrics: Arc<authz::metrics::MetricsCollector>,
516    seed: RefreshWorkerSeed,
517) -> Option<Arc<PolicyStoreRefreshHandle>> {
518    if !config.policy_store_config.refresh_enabled() {
519        return None;
520    }
521    let source = RefreshSource::from_policy_store_source(&config.policy_store_config.source)?;
522    let (interval_secs, clamped) = config.policy_store_config.effective_refresh_interval();
523    if clamped {
524        log.log_any(
525            LogEntry::new(BaseLogEntry::new_system_opt_request_id(LogLevel::WARN, None))
526                .set_message(format!(
527                    "CEDARLING_POLICY_STORE_REFRESH_INTERVAL={} is below the minimum; clamped to {} seconds",
528                    config.policy_store_config.refresh_interval_secs,
529                    interval_secs,
530                )),
531        );
532    }
533    let rebuilder = AuthzRebuilder {
534        jwt_config: config.jwt_config.clone(),
535        authorization_config: config.authorization_config.clone(),
536        http_client: service_factory.http_client_for_refresh(),
537        log: log.clone(),
538        data_store: data,
539        metrics: metrics.clone(),
540    };
541    let ctx = WorkerContext {
542        source,
543        interval_secs,
544        http_client: service_factory.http_client_for_refresh(),
545        rebuilder,
546        authz_swap,
547        metrics,
548        log,
549        initial_body_hash: seed.initial_body_hash,
550        initial_validators: seed.initial_validators,
551        strict_schema_validation: config.authorization_config.strict_schema_validation,
552    };
553    Some(Arc::new(spawn_refresh_worker(ctx)))
554}
555
556/// Log detailed information about the loaded policy store metadata, including
557/// ID, version, description, Cedar version, timestamps, and compatibility with
558/// the runtime Cedar version.
559fn log_policy_store_metadata(
560    log: &log::Logger,
561    metadata: &crate::common::policy_store::PolicyStoreMetadata,
562) {
563    // Build detailed log message using accessor methods
564    let mut details = format!(
565        "Policy store '{}' (ID: {}) v{} loaded",
566        metadata.name(),
567        if metadata.id().is_empty() {
568            "<auto>"
569        } else {
570            metadata.id()
571        },
572        metadata.version()
573    );
574
575    // Add description if available
576    if let Some(desc) = metadata.description() {
577        let _ = write!(details, " - {desc}");
578    }
579
580    // Add Cedar version info
581    let _ = write!(details, " [Cedar {}]", metadata.cedar_version());
582
583    // Add timestamp info if available
584    if let Some(created) = metadata.created_date() {
585        let _ = write!(details, " (created: {})", created.format("%Y-%m-%d"));
586    }
587    if let Some(updated) = metadata.updated_date() {
588        let _ = write!(details, " (updated: {})", updated.format("%Y-%m-%d"));
589    }
590
591    log.log_any(
592        LogEntry::new(BaseLogEntry::new_system_opt_request_id(
593            LogLevel::DEBUG,
594            None,
595        ))
596        .set_message(details),
597    );
598
599    // Log version compatibility check with current Cedar
600    let current_cedar_version: Version = cedar_policy::get_lang_version();
601    match metadata.is_compatible_with_cedar(&current_cedar_version) {
602        Ok(true) => {
603            log.log_any(
604                LogEntry::new(BaseLogEntry::new_system_opt_request_id(
605                    LogLevel::DEBUG,
606                    None,
607                ))
608                .set_message(format!(
609                    "Policy store Cedar version {} is compatible with runtime version {}",
610                    metadata.cedar_version(),
611                    current_cedar_version
612                )),
613            );
614        },
615        Ok(false) => {
616            log.log_any(
617                LogEntry::new(BaseLogEntry::new_system_opt_request_id(
618                    LogLevel::WARN,
619                    None,
620                ))
621                .set_message(format!(
622                    "Policy store Cedar version {} may not be compatible with runtime version {}",
623                    metadata.cedar_version(),
624                    current_cedar_version
625                )),
626            );
627        },
628        Err(e) => {
629            log.log_any(
630                LogEntry::new(BaseLogEntry::new_system_opt_request_id(
631                    LogLevel::WARN,
632                    None,
633                ))
634                .set_message(format!("Could not check Cedar version compatibility: {e}")),
635            );
636        },
637    }
638
639    // Log parsed version for debugging if available
640    if let Some(parsed_version) = metadata.version_parsed() {
641        log.log_any(
642            LogEntry::new(BaseLogEntry::new_system_opt_request_id(
643                LogLevel::TRACE,
644                None,
645            ))
646            .set_message(format!(
647                "Policy store semantic version: {}.{}.{}",
648                parsed_version.major, parsed_version.minor, parsed_version.patch
649            )),
650        );
651    }
652}
653
654// implements LogStorage for Cedarling
655// we can use this methods outside crate only when import trait
656impl LogStorage for Cedarling {
657    fn pop_logs(&self) -> Vec<serde_json::Value> {
658        self.log.pop_logs()
659    }
660
661    fn get_log_by_id(&self, id: &str) -> Option<serde_json::Value> {
662        self.log.get_log_by_id(id)
663    }
664
665    fn get_log_ids(&self) -> Vec<String> {
666        self.log.get_log_ids()
667    }
668
669    fn get_logs_by_tag(&self, tag: &str) -> Vec<serde_json::Value> {
670        self.log.get_logs_by_tag(tag)
671    }
672
673    fn get_logs_by_request_id(&self, request_id: &str) -> Vec<serde_json::Value> {
674        self.log.get_logs_by_request_id(request_id)
675    }
676
677    fn get_logs_by_request_id_and_tag(&self, id: &str, tag: &str) -> Vec<serde_json::Value> {
678        self.log.get_logs_by_request_id_and_tag(id, tag)
679    }
680}
681
682// implements DataApi for Cedarling
683// Helper function to calculate capacity usage and check memory alert threshold
684fn calculate_capacity_usage(
685    entry_count: usize,
686    max_entries: usize,
687    memory_alert_threshold: f64,
688) -> (f64, bool) {
689    // Precision loss is acceptable for percentage calculation
690    #[allow(clippy::cast_precision_loss)]
691    let capacity_usage_percent = if max_entries > 0 {
692        (entry_count as f64 / max_entries as f64) * 100.0
693    } else {
694        0.0 // Unlimited capacity, no percentage
695    };
696    let memory_alert_triggered = capacity_usage_percent >= memory_alert_threshold;
697    (capacity_usage_percent, memory_alert_triggered)
698}
699
700// provides public interface for pushing and retrieving data
701impl DataApi for Cedarling {
702    fn push_data_ctx(
703        &self,
704        key: &str,
705        value: serde_json::Value,
706        ttl: Option<std::time::Duration>,
707    ) -> Result<(), DataError> {
708        self.data.push(key, value, ttl)?;
709
710        // Check memory usage and log warning if threshold is exceeded
711        let config = self.data.config();
712        if config.max_entries > 0 {
713            let entry_count = self.data.count();
714            let (capacity_usage_percent, memory_alert_triggered) = calculate_capacity_usage(
715                entry_count,
716                config.max_entries,
717                config.memory_alert_threshold,
718            );
719            if memory_alert_triggered {
720                let log_entry = LogEntry::new(BaseLogEntry::new_system_opt_request_id(
721                    LogLevel::WARN,
722                    None,
723                ))
724                .set_message(format!(
725                    "DataStore memory usage alert: {:.1}% capacity used ({}/{} entries), threshold: {:.1}%",
726                    capacity_usage_percent,
727                    entry_count,
728                    config.max_entries,
729                    config.memory_alert_threshold
730                ));
731                self.log.log_any(log_entry);
732            }
733        }
734
735        Ok(())
736    }
737
738    fn get_data_ctx(&self, key: &str) -> Result<Option<serde_json::Value>, DataError> {
739        Ok(self.data.get(key))
740    }
741
742    fn get_data_entry_ctx(&self, key: &str) -> Result<Option<DataEntry>, DataError> {
743        Ok(self.data.get_entry(key))
744    }
745
746    fn remove_data_ctx(&self, key: &str) -> Result<bool, DataError> {
747        Ok(self.data.remove(key))
748    }
749
750    fn clear_data_ctx(&self) -> Result<(), DataError> {
751        self.data.clear();
752        Ok(())
753    }
754
755    fn list_data_ctx(&self) -> Result<Vec<DataEntry>, DataError> {
756        Ok(self.data.list_entries())
757    }
758
759    fn get_stats_ctx(&self) -> Result<DataStoreStats, DataError> {
760        let config = self.data.config();
761        let entry_count = self.data.count();
762        let total_size_bytes = self.data.total_size();
763        let avg_entry_size_bytes = total_size_bytes.checked_div(entry_count).unwrap_or(0);
764
765        // Calculate capacity usage percentage
766        let (capacity_usage_percent, memory_alert_triggered) = calculate_capacity_usage(
767            entry_count,
768            config.max_entries,
769            config.memory_alert_threshold,
770        );
771
772        Ok(DataStoreStats {
773            entry_count,
774            max_entries: config.max_entries,
775            max_entry_size: config.max_entry_size,
776            metrics_enabled: config.enable_metrics,
777            total_size_bytes,
778            avg_entry_size_bytes,
779            capacity_usage_percent,
780            memory_alert_threshold: config.memory_alert_threshold,
781            memory_alert_triggered,
782        })
783    }
784}