1#![deny(missing_docs)]
10
11use std::{fmt, str::FromStr};
12
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
18#[serde(rename_all = "lowercase")]
19pub enum DeploymentEnvironment {
20 #[default]
22 Local,
23 Testing,
25 Staging,
27 Production,
29}
30
31impl DeploymentEnvironment {
32 const fn as_str(self) -> &'static str {
33 match self {
34 Self::Local => "local",
35 Self::Testing => "testing",
36 Self::Staging => "staging",
37 Self::Production => "production",
38 }
39 }
40}
41
42impl fmt::Display for DeploymentEnvironment {
43 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44 formatter.write_str(self.as_str())
45 }
46}
47
48impl FromStr for DeploymentEnvironment {
49 type Err = ParseEnvironmentError;
50
51 fn from_str(value: &str) -> Result<Self, Self::Err> {
52 match value {
53 "local" => Ok(Self::Local),
54 "testing" => Ok(Self::Testing),
55 "staging" => Ok(Self::Staging),
56 "production" => Ok(Self::Production),
57 _ => Err(ParseEnvironmentError(value.to_owned())),
58 }
59 }
60}
61
62#[derive(Clone, Debug, Error, Eq, PartialEq)]
64#[error("unsupported environment `{0}`; expected local, testing, staging, or production")]
65pub struct ParseEnvironmentError(String);
66
67#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
69#[serde(rename_all = "lowercase")]
70pub enum LogFormat {
71 #[default]
73 Auto,
74 Json,
76 Pretty,
78}
79
80impl LogFormat {
81 #[must_use]
83 pub const fn resolve(self, environment: DeploymentEnvironment) -> Self {
84 match (self, environment) {
85 (Self::Auto, DeploymentEnvironment::Local) => Self::Pretty,
86 (
87 Self::Auto,
88 DeploymentEnvironment::Testing
89 | DeploymentEnvironment::Staging
90 | DeploymentEnvironment::Production,
91 ) => Self::Json,
92 (explicit, _) => explicit,
93 }
94 }
95}
96
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum ProcessKind {
100 Api,
102 Worker,
104 Migrate,
106 Seed,
108}
109
110impl ProcessKind {
111 const fn as_str(self) -> &'static str {
112 match self {
113 Self::Api => "api",
114 Self::Worker => "worker",
115 Self::Migrate => "migrate",
116 Self::Seed => "seed",
117 }
118 }
119}
120
121impl fmt::Display for ProcessKind {
122 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123 formatter.write_str(self.as_str())
124 }
125}
126
127#[derive(Clone, Debug, Eq, PartialEq)]
129pub struct ServiceIdentity {
130 product: String,
131 process: ProcessKind,
132 version: String,
133 commit: String,
134 environment: DeploymentEnvironment,
135}
136
137impl ServiceIdentity {
138 pub fn new(
140 product: impl Into<String>,
141 process: ProcessKind,
142 version: impl Into<String>,
143 commit: impl Into<String>,
144 environment: DeploymentEnvironment,
145 ) -> Self {
146 Self {
147 product: product.into(),
148 process,
149 version: version.into(),
150 commit: commit.into(),
151 environment,
152 }
153 }
154
155 pub fn product(&self) -> &str {
157 &self.product
158 }
159
160 pub const fn process(&self) -> ProcessKind {
162 self.process
163 }
164
165 pub fn version(&self) -> &str {
167 &self.version
168 }
169
170 pub fn commit(&self) -> &str {
172 &self.commit
173 }
174
175 pub const fn environment(&self) -> DeploymentEnvironment {
177 self.environment
178 }
179
180 pub fn service_name(&self) -> String {
182 format!("{}-{}", self.product, self.process)
183 }
184}
185
186#[derive(Clone, Debug, Eq, PartialEq)]
188pub struct BuildInfo {
189 version: String,
190 commit: String,
191 rust_version: String,
192}
193
194impl BuildInfo {
195 pub fn new(
197 version: impl Into<String>,
198 commit: impl Into<String>,
199 rust_version: impl Into<String>,
200 ) -> Self {
201 Self {
202 version: version.into(),
203 commit: commit.into(),
204 rust_version: rust_version.into(),
205 }
206 }
207
208 pub fn version(&self) -> &str {
210 &self.version
211 }
212
213 pub fn commit(&self) -> &str {
215 &self.commit
216 }
217
218 pub fn rust_version(&self) -> &str {
220 &self.rust_version
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn shared_vocabulary_follows_the_telemetry_contract() {
230 let identity = ServiceIdentity::new(
231 "orders",
232 ProcessKind::Api,
233 "1.2.3",
234 "abc123",
235 DeploymentEnvironment::Production,
236 );
237
238 assert_eq!(identity.service_name(), "orders-api");
239 assert_eq!(identity.environment().to_string(), "production");
240 assert_eq!(
241 LogFormat::Auto.resolve(DeploymentEnvironment::Production),
242 LogFormat::Json
243 );
244 }
245
246 #[test]
247 fn deployment_environments_parse_display_and_serialize() {
248 for (text, environment) in [
249 ("local", DeploymentEnvironment::Local),
250 ("testing", DeploymentEnvironment::Testing),
251 ("staging", DeploymentEnvironment::Staging),
252 ("production", DeploymentEnvironment::Production),
253 ] {
254 assert_eq!(text.parse::<DeploymentEnvironment>(), Ok(environment));
255 assert_eq!(environment.to_string(), text);
256 assert_eq!(
257 serde_json::to_string(&environment).expect("serialize environment"),
258 format!("\"{text}\"")
259 );
260 assert_eq!(
261 serde_json::from_str::<DeploymentEnvironment>(&format!("\"{text}\""))
262 .expect("deserialize environment"),
263 environment
264 );
265 }
266 }
267}
268
269#[doc = include_str!("../README.md")]
271#[cfg(doctest)]
272struct ReadmeDoctests;