1#![deny(missing_docs)]
10
11pub mod limits;
12
13use std::{fmt, str::FromStr};
14
15use serde::{Deserialize, Serialize};
16use thiserror::Error;
17
18#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
20#[serde(rename_all = "lowercase")]
21pub enum DeploymentEnvironment {
22 #[default]
24 Local,
25 Testing,
27 Staging,
29 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#[derive(Clone, Debug, Error, Eq, PartialEq)]
66#[error("unsupported environment `{0}`; expected local, testing, staging, or production")]
67pub struct ParseEnvironmentError(String);
68
69#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
71#[serde(rename_all = "lowercase")]
72pub enum LogFormat {
73 #[default]
75 Auto,
76 Json,
78 Pretty,
80}
81
82impl LogFormat {
83 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub enum ProcessKind {
102 Api,
104 Worker,
106 Migrate,
108 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#[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 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 pub fn product(&self) -> &str {
159 &self.product
160 }
161
162 pub const fn process(&self) -> ProcessKind {
164 self.process
165 }
166
167 pub fn version(&self) -> &str {
169 &self.version
170 }
171
172 pub fn commit(&self) -> &str {
174 &self.commit
175 }
176
177 pub const fn environment(&self) -> DeploymentEnvironment {
179 self.environment
180 }
181
182 pub fn service_name(&self) -> String {
184 format!("{}-{}", self.product, self.process)
185 }
186}
187
188#[derive(Clone, Debug, Eq, PartialEq)]
190pub struct BuildInfo {
191 version: String,
192 commit: String,
193 rust_version: String,
194}
195
196impl BuildInfo {
197 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 pub fn version(&self) -> &str {
212 &self.version
213 }
214
215 pub fn commit(&self) -> &str {
217 &self.commit
218 }
219
220 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#[doc = include_str!("../README.md")]
273#[cfg(doctest)]
274struct ReadmeDoctests;