Skip to main content

bge_m3_embedding_server/
logging.rs

1// Copyright (c) 2026 J. Patrick Fulton
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Tracing initialization with module and build-variant tags on every JSON log line.
16//!
17//! In `CloudWatch` / non-TTY environments the server emits one JSON object per
18//! log event. Every JSON event begins with two compile-time attributes:
19//!
20//! 1. `"bge_module"` — always `"server"`. Lets operators distinguish this
21//!    service from the router or other BGE-family processes in shared log groups.
22//! 2. `"build"` — `"cpu"` for the default MLAS image, `"cuda"` whenever the
23//!    `cuda` or `tensorrt` features are enabled (both are turned on by
24//!    `Dockerfile.cuda`). Use this in `CloudWatch` Insights to filter a mixed
25//!    CPU/CUDA fleet.
26//!
27//! The formatter chain is `PrependModule → PrependBuild → JSON formatter`,
28//! so the rendered line always starts with
29//! `{"bge_module":"server","build":"<variant>",…}`.
30//!
31//! The human-readable `text` / `pretty` formats used during local dev are
32//! left unchanged.
33
34use std::fmt;
35
36use tracing::{Event, Subscriber};
37use tracing_subscriber::fmt::FmtContext;
38use tracing_subscriber::fmt::format::{FormatEvent, FormatFields, Writer};
39use tracing_subscriber::registry::LookupSpan;
40
41/// Compile-time service name included as the first key on every JSON log line.
42///
43/// Distinguishes this binary from the BGE router and other services that may
44/// share a `CloudWatch` log group or Logs Insights workspace.
45pub const BGE_MODULE: &str = "server";
46
47/// Compile-time identifier for the build of the server that is running.
48///
49/// Resolves to `"cuda"` when either the `cuda` or `tensorrt` Cargo feature is
50/// enabled — the production CUDA image (`Dockerfile.cuda`) turns on both, and
51/// operators care about "is this the GPU image?" not "which EP variant did it
52/// register". Otherwise resolves to `"cpu"` (the default MLAS build).
53pub const BUILD_VARIANT: &str = if cfg!(any(feature = "cuda", feature = "tensorrt")) {
54    "cuda"
55} else {
56    "cpu"
57};
58
59/// Wraps a JSON [`FormatEvent`] so the rendered object always begins with a
60/// `"build"` key.
61///
62/// The wrapper renders the inner formatter into a temporary `String`, then
63/// rewrites the leading `{` to `{"build":"<variant>",`. All other keys, span
64/// context, and trailing newline produced by the inner formatter are
65/// preserved verbatim, so existing `CloudWatch` Insights queries keep working.
66pub struct PrependBuild<F> {
67    inner: F,
68}
69
70impl<F> PrependBuild<F> {
71    /// Wrap `inner` so its rendered events are prefixed with the build key.
72    pub fn new(inner: F) -> Self {
73        Self { inner }
74    }
75}
76
77impl<S, N, F> FormatEvent<S, N> for PrependBuild<F>
78where
79    S: Subscriber + for<'a> LookupSpan<'a>,
80    N: for<'a> FormatFields<'a> + 'static,
81    F: FormatEvent<S, N>,
82{
83    fn format_event(
84        &self,
85        ctx: &FmtContext<'_, S, N>,
86        mut writer: Writer<'_>,
87        event: &Event<'_>,
88    ) -> fmt::Result {
89        let mut buf = String::new();
90        self.inner.format_event(ctx, Writer::new(&mut buf), event)?;
91
92        let trailing_newline = buf.ends_with('\n');
93        let body = if trailing_newline {
94            &buf[..buf.len() - 1]
95        } else {
96            &buf[..]
97        };
98
99        if let Some(rest) = body.strip_prefix('{') {
100            if let Some(rest) = rest.strip_prefix('}') {
101                // Inner produced an empty object `{}` — emit `{"build":"…"}`
102                // followed by whatever (if anything) trailed the close brace.
103                write!(writer, "{{\"build\":\"{BUILD_VARIANT}\"}}{rest}")?;
104            } else {
105                write!(writer, "{{\"build\":\"{BUILD_VARIANT}\",{rest}")?;
106            }
107        } else {
108            // Inner formatter did not produce a JSON object (e.g. someone
109            // wrapped the wrong formatter). Pass the body through unchanged
110            // rather than corrupting it.
111            writer.write_str(body)?;
112        }
113
114        if trailing_newline {
115            writer.write_char('\n')?;
116        }
117        Ok(())
118    }
119}
120
121/// Wraps a JSON [`FormatEvent`] so the rendered object always begins with a
122/// `"bge_module"` key (value: the compile-time [`BGE_MODULE`] constant).
123///
124/// Intended to be the outermost wrapper in the formatter chain so that
125/// `"bge_module"` appears as the very first key, before `"build"` and before
126/// all standard `tracing-subscriber` fields.
127pub struct PrependModule<F> {
128    inner: F,
129}
130
131impl<F> PrependModule<F> {
132    /// Wrap `inner` so its rendered events are prefixed with the module key.
133    pub fn new(inner: F) -> Self {
134        Self { inner }
135    }
136}
137
138impl<S, N, F> FormatEvent<S, N> for PrependModule<F>
139where
140    S: Subscriber + for<'a> LookupSpan<'a>,
141    N: for<'a> FormatFields<'a> + 'static,
142    F: FormatEvent<S, N>,
143{
144    fn format_event(
145        &self,
146        ctx: &FmtContext<'_, S, N>,
147        mut writer: Writer<'_>,
148        event: &Event<'_>,
149    ) -> fmt::Result {
150        let mut buf = String::new();
151        self.inner.format_event(ctx, Writer::new(&mut buf), event)?;
152
153        let trailing_newline = buf.ends_with('\n');
154        let body = if trailing_newline {
155            &buf[..buf.len() - 1]
156        } else {
157            &buf[..]
158        };
159
160        if let Some(rest) = body.strip_prefix('{') {
161            if let Some(rest) = rest.strip_prefix('}') {
162                // Inner produced `{}` — emit `{"bge_module":"…"}` plus any tail.
163                write!(writer, "{{\"bge_module\":\"{BGE_MODULE}\"}}{rest}")?;
164            } else {
165                write!(writer, "{{\"bge_module\":\"{BGE_MODULE}\",{rest}")?;
166            }
167        } else {
168            // Inner formatter did not produce a JSON object. Pass through unchanged.
169            writer.write_str(body)?;
170        }
171
172        if trailing_newline {
173            writer.write_char('\n')?;
174        }
175        Ok(())
176    }
177}
178
179/// Initialize the global tracing subscriber.
180///
181/// Reads `RUST_LOG` for the filter directive (defaulting to `info`) and
182/// `BGE_M3_LOG_FORMAT` for the format selection: `json`, `text`/`pretty`, or
183/// auto-detect via the stdout TTY check. JSON events begin with
184/// `"bge_module":"server"` then `"build":"<variant>"` as the first two keys;
185/// the human formats are unchanged.
186///
187/// Calling this twice is harmless — the second call no-ops via `try_init`.
188pub fn init() {
189    let env_filter =
190        tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into());
191    let log_format = std::env::var("BGE_M3_LOG_FORMAT").ok();
192    // JSON by default in non-TTY environments (Docker / Fargate / CloudWatch).
193    // Force pretty with BGE_M3_LOG_FORMAT=text or BGE_M3_LOG_FORMAT=pretty.
194    // Force JSON with BGE_M3_LOG_FORMAT=json.
195    let want_json = match log_format.as_deref() {
196        Some("text" | "pretty") => false,
197        Some("json") => true,
198        _ => !std::io::IsTerminal::is_terminal(&std::io::stdout()),
199    };
200    if want_json {
201        let inner = tracing_subscriber::fmt::format()
202            .json()
203            .with_current_span(true);
204        tracing_subscriber::fmt()
205            .event_format(PrependModule::new(PrependBuild::new(inner)))
206            .fmt_fields(tracing_subscriber::fmt::format::JsonFields::new())
207            .with_env_filter(env_filter)
208            .try_init()
209            .ok();
210    } else {
211        tracing_subscriber::fmt()
212            .with_env_filter(env_filter)
213            .try_init()
214            .ok();
215    }
216}
217
218#[cfg(test)]
219mod tests;