Skip to main content

baukit_core/
lib.rs

1//! Dependency-light shared vocabulary for Baukit crates and services.
2//!
3//! This crate is the canonical home for dependency-light process vocabulary and
4//! runtime-neutral measurements shared across crate boundaries. It deliberately
5//! contains no telemetry exporters, async runtime, HTTP framework, operational
6//! routing, or product policy. Higher-level Baukit crates re-export established
7//! compatibility types from their own APIs.
8
9#![deny(missing_docs)]
10
11pub mod limits;
12
13use std::{fmt, str::FromStr};
14
15use serde::{Deserialize, Serialize};
16use thiserror::Error;
17
18/// The deployment environment attached to configuration and telemetry signals.
19#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
20#[serde(rename_all = "lowercase")]
21pub enum DeploymentEnvironment {
22    /// A developer workstation or other local process.
23    #[default]
24    Local,
25    /// A deployed environment used for integration and acceptance testing.
26    Testing,
27    /// A deployed pre-production process.
28    Staging,
29    /// A deployed production process.
30    Production,
31}
32
33impl DeploymentEnvironment {
34    const fn as_str(self) -> &'static str {
35        match self {
36            Self::Local => "local",
37            Self::Testing => "testing",
38            Self::Staging => "staging",
39            Self::Production => "production",
40        }
41    }
42}
43
44impl fmt::Display for DeploymentEnvironment {
45    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46        formatter.write_str(self.as_str())
47    }
48}
49
50impl FromStr for DeploymentEnvironment {
51    type Err = ParseEnvironmentError;
52
53    fn from_str(value: &str) -> Result<Self, Self::Err> {
54        match value {
55            "local" => Ok(Self::Local),
56            "testing" => Ok(Self::Testing),
57            "staging" => Ok(Self::Staging),
58            "production" => Ok(Self::Production),
59            _ => Err(ParseEnvironmentError(value.to_owned())),
60        }
61    }
62}
63
64/// An error returned when a deployment environment name is not supported.
65#[derive(Clone, Debug, Error, Eq, PartialEq)]
66#[error("unsupported environment `{0}`; expected local, testing, staging, or production")]
67pub struct ParseEnvironmentError(String);
68
69/// Selection policy for the stdout log formatter.
70#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
71#[serde(rename_all = "lowercase")]
72pub enum LogFormat {
73    /// Select JSON for deployed environments, and pretty output locally.
74    #[default]
75    Auto,
76    /// Always emit newline-delimited JSON.
77    Json,
78    /// Always emit human-readable pretty output.
79    Pretty,
80}
81
82impl LogFormat {
83    /// Resolves [`LogFormat::Auto`] for a deployment environment.
84    #[must_use]
85    pub const fn resolve(self, environment: DeploymentEnvironment) -> Self {
86        match (self, environment) {
87            (Self::Auto, DeploymentEnvironment::Local) => Self::Pretty,
88            (
89                Self::Auto,
90                DeploymentEnvironment::Testing
91                | DeploymentEnvironment::Staging
92                | DeploymentEnvironment::Production,
93            ) => Self::Json,
94            (explicit, _) => explicit,
95        }
96    }
97}
98
99/// The kind of process being identified and instrumented.
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub enum ProcessKind {
102    /// A public or internal HTTP API process.
103    Api,
104    /// A background worker process.
105    Worker,
106    /// A database migration process.
107    Migrate,
108    /// A data seeding process.
109    Seed,
110}
111
112impl ProcessKind {
113    const fn as_str(self) -> &'static str {
114        match self {
115            Self::Api => "api",
116            Self::Worker => "worker",
117            Self::Migrate => "migrate",
118            Self::Seed => "seed",
119        }
120    }
121}
122
123impl fmt::Display for ProcessKind {
124    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125        formatter.write_str(self.as_str())
126    }
127}
128
129/// Stable identity and build metadata for one running process.
130#[derive(Clone, Debug, Eq, PartialEq)]
131pub struct ServiceIdentity {
132    product: String,
133    process: ProcessKind,
134    version: String,
135    commit: String,
136    environment: DeploymentEnvironment,
137}
138
139impl ServiceIdentity {
140    /// Creates a service identity.
141    pub fn new(
142        product: impl Into<String>,
143        process: ProcessKind,
144        version: impl Into<String>,
145        commit: impl Into<String>,
146        environment: DeploymentEnvironment,
147    ) -> Self {
148        Self {
149            product: product.into(),
150            process,
151            version: version.into(),
152            commit: commit.into(),
153            environment,
154        }
155    }
156
157    /// Returns the stable product identifier.
158    pub fn product(&self) -> &str {
159        &self.product
160    }
161
162    /// Returns this process's kind.
163    pub const fn process(&self) -> ProcessKind {
164        self.process
165    }
166
167    /// Returns the Cargo package version.
168    pub fn version(&self) -> &str {
169        &self.version
170    }
171
172    /// Returns the short source-control commit identifier.
173    pub fn commit(&self) -> &str {
174        &self.commit
175    }
176
177    /// Returns the deployment environment.
178    pub const fn environment(&self) -> DeploymentEnvironment {
179        self.environment
180    }
181
182    /// Composes the OpenTelemetry `service.name` value.
183    pub fn service_name(&self) -> String {
184        format!("{}-{}", self.product, self.process)
185    }
186}
187
188/// Immutable version-control and compiler metadata for a process build.
189#[derive(Clone, Debug, Eq, PartialEq)]
190pub struct BuildInfo {
191    version: String,
192    commit: String,
193    rust_version: String,
194}
195
196impl BuildInfo {
197    /// Creates build metadata from values supplied by a build pipeline.
198    pub fn new(
199        version: impl Into<String>,
200        commit: impl Into<String>,
201        rust_version: impl Into<String>,
202    ) -> Self {
203        Self {
204            version: version.into(),
205            commit: commit.into(),
206            rust_version: rust_version.into(),
207        }
208    }
209
210    /// Returns the Cargo package version.
211    pub fn version(&self) -> &str {
212        &self.version
213    }
214
215    /// Returns the source-control commit identifier.
216    pub fn commit(&self) -> &str {
217        &self.commit
218    }
219
220    /// Returns the Rust compiler version used for the build.
221    pub fn rust_version(&self) -> &str {
222        &self.rust_version
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn shared_vocabulary_follows_the_telemetry_contract() {
232        let identity = ServiceIdentity::new(
233            "orders",
234            ProcessKind::Api,
235            "1.2.3",
236            "abc123",
237            DeploymentEnvironment::Production,
238        );
239
240        assert_eq!(identity.service_name(), "orders-api");
241        assert_eq!(identity.environment().to_string(), "production");
242        assert_eq!(
243            LogFormat::Auto.resolve(DeploymentEnvironment::Production),
244            LogFormat::Json
245        );
246    }
247
248    #[test]
249    fn deployment_environments_parse_display_and_serialize() {
250        for (text, environment) in [
251            ("local", DeploymentEnvironment::Local),
252            ("testing", DeploymentEnvironment::Testing),
253            ("staging", DeploymentEnvironment::Staging),
254            ("production", DeploymentEnvironment::Production),
255        ] {
256            assert_eq!(text.parse::<DeploymentEnvironment>(), Ok(environment));
257            assert_eq!(environment.to_string(), text);
258            assert_eq!(
259                serde_json::to_string(&environment).expect("serialize environment"),
260                format!("\"{text}\"")
261            );
262            assert_eq!(
263                serde_json::from_str::<DeploymentEnvironment>(&format!("\"{text}\""))
264                    .expect("deserialize environment"),
265                environment
266            );
267        }
268    }
269}
270
271// Compiles the README's examples so they cannot drift from the API.
272#[doc = include_str!("../README.md")]
273#[cfg(doctest)]
274struct ReadmeDoctests;