azalia_log/
lib.rs

1// 🐻‍❄️🪚 azalia: Noelware's Rust commons library.
2// Copyright (c) 2024-2025 Noelware, LLC. <team@noelware.org>
3//
4// Permission is hereby granted, free of charge, to any person obtaining a copy
5// of this software and associated documentation files (the "Software"), to deal
6// in the Software without restriction, including without limitation the rights
7// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8// copies of the Software, and to permit persons to whom the Software is
9// furnished to do so, subject to the following conditions:
10//
11// The above copyright notice and this permission notice shall be included in all
12// copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20// SOFTWARE.
21
22//! # 🐻‍❄️🪚 `azalia-log`
23//! The **log** Rust crate provides a JSON-compat writer that mimics the output of Logstash
24//! and a beautiful prettified logger that is used in all Noelware products and services.
25
26#![doc(html_logo_url = "https://cdn.floofy.dev/images/trans.png")]
27#![doc(html_favicon_url = "https://cdn.floofy.dev/images/trans.png")]
28#![cfg_attr(any(noeldoc, docsrs), feature(doc_cfg))]
29
30#[cfg(feature = "writers")]
31#[cfg_attr(any(docsrs, noeldoc), doc(cfg(feature = "writers")))]
32pub mod writers;
33
34#[cfg(not(feature = "writers"))]
35mod writers;
36
37#[cfg(not(feature = "writers"))]
38pub use writers::JsonVisitor;
39
40use std::{io::Write, sync::RwLock};
41use tracing::{span, Event, Metadata, Subscriber};
42use tracing_subscriber::{
43    registry::{LookupSpan, SpanRef},
44    Layer,
45};
46
47/// Represents a function-based trait to create a [`String`] buffer with pieces you might need. This shouldn't
48/// be implemented directly, but can be written with the following function signature:
49///
50/// ```rust,ignore
51/// fn(&tracing::Event, &tracing::Metadata, Vec<serde_json::Value>) -> Result<String, std::fmt::Error>
52/// ```
53pub trait WriteFn<S: for<'l> LookupSpan<'l>>: Send {
54    fn buffer(&self, event: &Event, metadata: &Metadata, spans: Vec<SpanRef<'_, S>>) -> String;
55}
56
57impl<S: for<'l> LookupSpan<'l>, F> WriteFn<S> for F
58where
59    F: Fn(&Event, &Metadata, Vec<SpanRef<'_, S>>) -> String + Send,
60{
61    fn buffer(&self, event: &Event, metadata: &Metadata, spans: Vec<SpanRef<'_, S>>) -> String {
62        (self)(event, metadata, spans)
63    }
64}
65
66/// Represents a [`Layer`] for writing to a type that implements [`Write`], with a optional
67/// [`WriteFn`] to go alongside with this type.
68pub struct WriteLayer<S: for<'l> LookupSpan<'l>> {
69    writer: RwLock<Box<dyn Write + Send + Sync>>,
70    write_fn: Option<Box<dyn WriteFn<S> + Send + Sync>>,
71}
72
73impl<S: for<'l> LookupSpan<'l>> WriteLayer<S> {
74    /// Creates a new [`WriteLayer`] without a [`WriteFn`].
75    pub fn new<W: Write + Send + Sync + 'static>(writer: W) -> WriteLayer<S> {
76        WriteLayer {
77            writer: RwLock::new(Box::new(writer)),
78            write_fn: None,
79        }
80    }
81
82    /// Creates a new [`WriteLayer`] with a specified [`WriteFn`].
83    pub fn new_with<W: Write + Send + Sync + 'static, F: WriteFn<S> + Send + Sync + 'static>(
84        writer: W,
85        fn_: F,
86    ) -> WriteLayer<S> {
87        WriteLayer {
88            writer: RwLock::new(Box::new(writer)),
89            write_fn: Some(Box::new(fn_)),
90        }
91    }
92}
93
94#[derive(Debug)]
95pub(crate) struct JsonExtension(pub(crate) std::collections::BTreeMap<String, serde_json::Value>);
96impl<S: Subscriber + for<'l> LookupSpan<'l>> Layer<S> for WriteLayer<S> {
97    fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
98        let span = ctx.span(id).unwrap();
99        let mut data = std::collections::BTreeMap::new();
100
101        let mut visitor = crate::writers::JsonVisitor(&mut data);
102        attrs.record(&mut visitor);
103
104        span.extensions_mut().insert(JsonExtension(data));
105    }
106
107    fn on_record(&self, span: &span::Id, values: &span::Record<'_>, ctx: tracing_subscriber::layer::Context<'_, S>) {
108        let span = ctx.span(span).unwrap();
109        let mut exts = span.extensions_mut();
110        let data: &mut JsonExtension = exts.get_mut::<JsonExtension>().unwrap();
111
112        let mut visitor = crate::writers::JsonVisitor(&mut data.0);
113        values.record(&mut visitor);
114    }
115
116    fn on_event(&self, event: &Event<'_>, ctx: tracing_subscriber::layer::Context<'_, S>) {
117        let mut writer = self.writer.write().unwrap();
118        if let Some(ref fn_) = self.write_fn {
119            cfg_if::cfg_if! {
120                if #[cfg(feature = "tracing-log")] {
121                    use tracing_log::NormalizeEvent;
122
123                    let metadata = event.normalized_metadata();
124                    let metadata = metadata.as_ref().unwrap_or_else(|| event.metadata());
125                } else {
126                    let metadata = event.metadata();
127                }
128            };
129
130            let mut spans: Vec<SpanRef<'_, S>> = vec![];
131            if let Some(scope) = ctx.event_scope(event) {
132                for span in scope.from_root() {
133                    spans.push(span);
134                }
135            }
136
137            let buf = fn_.buffer(event, metadata, spans);
138            let _ = write!(writer, "{buf}");
139            let _ = writeln!(writer);
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use crate::WriteLayer;
147    use std::io;
148    use tracing::Dispatch;
149    use tracing_subscriber::{layer::SubscriberExt, registry, Layer, Registry};
150
151    fn __assert_is_layer<S>(_: &dyn Layer<S>) {
152        /* no body here */
153    }
154
155    fn __assert_is_dispatchable(_: impl Into<Dispatch>) {
156        /* no body here :3 */
157    }
158
159    #[test]
160    fn assertions() {
161        __assert_is_layer::<Registry>(&WriteLayer::new(io::stdout()));
162        __assert_is_dispatchable(registry().with(WriteLayer::new(io::stdout())));
163
164        #[cfg(feature = "writers")]
165        __assert_is_dispatchable(registry().with(WriteLayer::new_with(
166            io::stdout(),
167            crate::writers::default::Writer::default(),
168        )));
169
170        #[cfg(feature = "writers")]
171        __assert_is_dispatchable(registry().with(WriteLayer::new_with(io::stdout(), crate::writers::json)));
172    }
173}