Skip to content

Commit 38991ed

Browse files
committed
feat(telemetry): Add a compatibility layer that emits app logs as tracing events.
Signed-off-by: Caleb Schoepp <caleb.schoepp@fermyon.com>
1 parent c44da5f commit 38991ed

File tree

7 files changed

+188
-68
lines changed

7 files changed

+188
-68
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/telemetry/src/env.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use opentelemetry_otlp::{
88
const OTEL_SDK_DISABLED: &str = "OTEL_SDK_DISABLED";
99
const OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL";
1010
const OTEL_EXPORTER_OTLP_METRICS_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL";
11+
const SPIN_DISABLE_LOG_TO_TRACING: &str = "SPIN_DISABLE_LOG_TO_TRACING";
1112

1213
/// Returns a boolean indicating if the OTEL tracing layer should be enabled.
1314
///
@@ -37,6 +38,15 @@ pub(crate) fn otel_metrics_enabled() -> bool {
3738
]) && !otel_sdk_disabled()
3839
}
3940

41+
/// Returns a boolean indicating if the compatibility layer that emits tracing events from
42+
/// applications logs should be disabled.
43+
///
44+
/// It is considered disabled if the environment variable `SPIN_DISABLED_LOG_TO_TRACING` is set and not
45+
/// empty. By default the features is enabled.
46+
pub(crate) fn spin_disable_log_to_tracing() -> bool {
47+
any_vars_set(&[SPIN_DISABLE_LOG_TO_TRACING])
48+
}
49+
4050
fn any_vars_set(enabling_vars: &[&str]) -> bool {
4151
enabling_vars
4252
.iter()

crates/telemetry/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use tracing_subscriber::{fmt, prelude::*, registry, EnvFilter, Layer};
77

88
pub mod detector;
99
mod env;
10+
pub mod log;
1011
pub mod metrics;
1112
mod propagation;
1213
mod traces;
@@ -51,9 +52,15 @@ pub fn init(spin_version: String) -> anyhow::Result<ShutdownGuard> {
5152
.with_writer(std::io::stderr)
5253
.with_ansi(std::io::stderr().is_terminal())
5354
.with_filter(
55+
// Filter directives explained here https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives
5456
EnvFilter::from_default_env()
57+
// Wasmtime is too noisy
5558
.add_directive("wasmtime_wasi_http=warn".parse()?)
56-
.add_directive("watchexec=off".parse()?),
59+
// Watchexec is too noisy
60+
.add_directive("watchexec=off".parse()?)
61+
// We don't want to duplicate application logs
62+
.add_directive("[{app_log}]=off".parse()?)
63+
.add_directive("[{app_log_non_utf8}]=off".parse()?),
5764
);
5865

5966
// Even if metrics or tracing aren't enabled we're okay to turn on the global error handler

crates/telemetry/src/log.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use std::{ascii::escape_default, sync::OnceLock};
2+
3+
use crate::env;
4+
5+
/// Takes a Spin application log and emits it as a tracing event.
6+
///
7+
/// This acts as a compatibility layer to easily get Spin app logs as events in our OTel traces.
8+
pub fn app_log_to_tracing_event(buf: &[u8]) {
9+
static CELL: OnceLock<bool> = OnceLock::new();
10+
if *CELL.get_or_init(env::spin_disable_log_to_tracing) {
11+
return;
12+
}
13+
14+
if let Ok(s) = std::str::from_utf8(buf) {
15+
tracing::info!(app_log = s);
16+
} else {
17+
tracing::info!(
18+
app_log_non_utf8 = buf
19+
.iter()
20+
.take(50)
21+
.map(|&x| escape_default(x).to_string())
22+
.collect::<String>()
23+
);
24+
}
25+
}

crates/trigger/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ spin-world = { path = "../world" }
4141
spin-llm = { path = "../llm" }
4242
spin-llm-local = { path = "../llm-local", optional = true }
4343
spin-llm-remote-http = { path = "../llm-remote-http" }
44+
spin-telemetry = { path = "../telemetry" }
4445
sanitize-filename = "0.4"
4546
serde = "1.0.188"
4647
serde_json = "1.0"

crates/trigger/src/stdio.rs

Lines changed: 142 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,19 @@ impl StdioLoggingTriggerHooks {
5656
&self,
5757
component_id: &str,
5858
log_suffix: &str,
59-
log_dir: &Path,
59+
log_dir: Option<&Path>,
6060
) -> Result<ComponentStdioWriter> {
6161
let sanitized_component_id = sanitize_filename::sanitize(component_id);
62-
let log_path = log_dir.join(format!("{sanitized_component_id}_{log_suffix}.txt"));
62+
let log_path = log_dir
63+
.map(|log_dir| log_dir.join(format!("{sanitized_component_id}_{log_suffix}.txt",)));
64+
let log_path = log_path.as_deref();
65+
6366
let follow = self.follow_components.should_follow(component_id);
64-
ComponentStdioWriter::new(&log_path, follow)
65-
.with_context(|| format!("Failed to open log file {}", quoted_path(&log_path)))
67+
match log_path {
68+
Some(log_path) => ComponentStdioWriter::new_forward(log_path, follow)
69+
.with_context(|| format!("Failed to open log file {}", quoted_path(log_path))),
70+
None => ComponentStdioWriter::new_inherit(),
71+
}
6672
}
6773

6874
fn validate_follows(&self, app: &spin_app::App) -> anyhow::Result<()> {
@@ -112,27 +118,37 @@ impl TriggerHooks for StdioLoggingTriggerHooks {
112118
component: &spin_app::AppComponent,
113119
builder: &mut spin_core::StoreBuilder,
114120
) -> anyhow::Result<()> {
115-
match &self.log_dir {
116-
Some(l) => {
117-
builder.stdout_pipe(self.component_stdio_writer(component.id(), "stdout", l)?);
118-
builder.stderr_pipe(self.component_stdio_writer(component.id(), "stderr", l)?);
119-
}
120-
None => {
121-
builder.inherit_stdout();
122-
builder.inherit_stderr();
123-
}
124-
}
121+
builder.stdout_pipe(self.component_stdio_writer(
122+
component.id(),
123+
"stdout",
124+
self.log_dir.as_deref(),
125+
)?);
126+
builder.stderr_pipe(self.component_stdio_writer(
127+
component.id(),
128+
"stderr",
129+
self.log_dir.as_deref(),
130+
)?);
125131

126132
Ok(())
127133
}
128134
}
129135

130-
/// ComponentStdioWriter forwards output to a log file and (optionally) stderr
136+
/// ComponentStdioWriter forwards output to a log file, (optionally) stderr, and (optionally) to a
137+
/// tracing compatibility layer.
131138
pub struct ComponentStdioWriter {
132-
sync_file: std::fs::File,
133-
async_file: tokio::fs::File,
134-
state: ComponentStdioWriterState,
135-
follow: bool,
139+
inner: ComponentStdioWriterInner,
140+
}
141+
142+
enum ComponentStdioWriterInner {
143+
/// Inherit stdout/stderr from the parent process.
144+
Inherit,
145+
/// Forward stdout/stderr to a file in addition to the inherited stdout/stderr.
146+
Forward {
147+
sync_file: std::fs::File,
148+
async_file: tokio::fs::File,
149+
state: ComponentStdioWriterState,
150+
follow: bool,
151+
},
136152
}
137153

138154
#[derive(Debug)]
@@ -142,20 +158,30 @@ enum ComponentStdioWriterState {
142158
}
143159

144160
impl ComponentStdioWriter {
145-
pub fn new(log_path: &Path, follow: bool) -> anyhow::Result<Self> {
161+
fn new_forward(log_path: &Path, follow: bool) -> anyhow::Result<Self> {
146162
let sync_file = std::fs::File::options()
147163
.create(true)
148164
.append(true)
149165
.open(log_path)?;
166+
150167
let async_file = sync_file
151168
.try_clone()
152169
.context("could not get async file handle")?
153170
.into();
171+
154172
Ok(Self {
155-
async_file,
156-
sync_file,
157-
state: ComponentStdioWriterState::File,
158-
follow,
173+
inner: ComponentStdioWriterInner::Forward {
174+
sync_file,
175+
async_file,
176+
state: ComponentStdioWriterState::File,
177+
follow,
178+
},
179+
})
180+
}
181+
182+
fn new_inherit() -> anyhow::Result<Self> {
183+
Ok(Self {
184+
inner: ComponentStdioWriterInner::Inherit,
159185
})
160186
}
161187
}
@@ -167,38 +193,56 @@ impl AsyncWrite for ComponentStdioWriter {
167193
buf: &[u8],
168194
) -> Poll<std::result::Result<usize, std::io::Error>> {
169195
let this = self.get_mut();
196+
170197
loop {
171-
match &this.state {
172-
ComponentStdioWriterState::File => {
198+
match &mut this.inner {
199+
ComponentStdioWriterInner::Inherit => {
173200
let written = futures::ready!(
174-
std::pin::Pin::new(&mut this.async_file).poll_write(cx, buf)
201+
std::pin::Pin::new(&mut tokio::io::stderr()).poll_write(cx, buf)
175202
);
176203
let written = match written {
177-
Ok(e) => e,
204+
Ok(w) => w,
178205
Err(e) => return Poll::Ready(Err(e)),
179206
};
180-
if this.follow {
181-
this.state = ComponentStdioWriterState::Follow(0..written);
182-
} else {
183-
return Poll::Ready(Ok(written));
184-
}
185-
}
186-
ComponentStdioWriterState::Follow(range) => {
187-
let written = futures::ready!(std::pin::Pin::new(&mut tokio::io::stderr())
188-
.poll_write(cx, &buf[range.clone()]));
189-
let written = match written {
190-
Ok(e) => e,
191-
Err(e) => return Poll::Ready(Err(e)),
192-
};
193-
if range.start + written >= range.end {
194-
let end = range.end;
195-
this.state = ComponentStdioWriterState::File;
196-
return Poll::Ready(Ok(end));
197-
} else {
198-
this.state =
199-
ComponentStdioWriterState::Follow((range.start + written)..range.end);
200-
};
207+
return Poll::Ready(Ok(written));
201208
}
209+
ComponentStdioWriterInner::Forward {
210+
async_file,
211+
state,
212+
follow,
213+
..
214+
} => match &state {
215+
ComponentStdioWriterState::File => {
216+
let written =
217+
futures::ready!(std::pin::Pin::new(async_file).poll_write(cx, buf));
218+
let written = match written {
219+
Ok(w) => w,
220+
Err(e) => return Poll::Ready(Err(e)),
221+
};
222+
if *follow {
223+
*state = ComponentStdioWriterState::Follow(0..written);
224+
} else {
225+
return Poll::Ready(Ok(written));
226+
}
227+
}
228+
ComponentStdioWriterState::Follow(range) => {
229+
let written = futures::ready!(std::pin::Pin::new(&mut tokio::io::stderr())
230+
.poll_write(cx, &buf[range.clone()]));
231+
let written = match written {
232+
Ok(w) => w,
233+
Err(e) => return Poll::Ready(Err(e)),
234+
};
235+
if range.start + written >= range.end {
236+
let end = range.end;
237+
*state = ComponentStdioWriterState::File;
238+
return Poll::Ready(Ok(end));
239+
} else {
240+
*state = ComponentStdioWriterState::Follow(
241+
(range.start + written)..range.end,
242+
);
243+
};
244+
}
245+
},
202246
}
203247
}
204248
}
@@ -208,13 +252,19 @@ impl AsyncWrite for ComponentStdioWriter {
208252
cx: &mut std::task::Context<'_>,
209253
) -> Poll<std::result::Result<(), std::io::Error>> {
210254
let this = self.get_mut();
211-
match this.state {
212-
ComponentStdioWriterState::File => {
213-
std::pin::Pin::new(&mut this.async_file).poll_flush(cx)
214-
}
215-
ComponentStdioWriterState::Follow(_) => {
255+
256+
match &mut this.inner {
257+
ComponentStdioWriterInner::Inherit => {
216258
std::pin::Pin::new(&mut tokio::io::stderr()).poll_flush(cx)
217259
}
260+
ComponentStdioWriterInner::Forward {
261+
async_file, state, ..
262+
} => match state {
263+
ComponentStdioWriterState::File => std::pin::Pin::new(async_file).poll_flush(cx),
264+
ComponentStdioWriterState::Follow(_) => {
265+
std::pin::Pin::new(&mut tokio::io::stderr()).poll_flush(cx)
266+
}
267+
},
218268
}
219269
}
220270

@@ -223,32 +273,57 @@ impl AsyncWrite for ComponentStdioWriter {
223273
cx: &mut std::task::Context<'_>,
224274
) -> Poll<std::result::Result<(), std::io::Error>> {
225275
let this = self.get_mut();
226-
match this.state {
227-
ComponentStdioWriterState::File => {
228-
std::pin::Pin::new(&mut this.async_file).poll_shutdown(cx)
229-
}
230-
ComponentStdioWriterState::Follow(_) => {
276+
277+
match &mut this.inner {
278+
ComponentStdioWriterInner::Inherit => {
231279
std::pin::Pin::new(&mut tokio::io::stderr()).poll_flush(cx)
232280
}
281+
ComponentStdioWriterInner::Forward {
282+
async_file, state, ..
283+
} => match state {
284+
ComponentStdioWriterState::File => std::pin::Pin::new(async_file).poll_shutdown(cx),
285+
ComponentStdioWriterState::Follow(_) => {
286+
std::pin::Pin::new(&mut tokio::io::stderr()).poll_flush(cx)
287+
}
288+
},
233289
}
234290
}
235291
}
236292

237293
impl std::io::Write for ComponentStdioWriter {
238294
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
239-
let written = self.sync_file.write(buf)?;
240-
if self.follow {
241-
std::io::stderr().write_all(&buf[..written])?;
295+
spin_telemetry::log::app_log_to_tracing_event(buf);
296+
297+
match &mut self.inner {
298+
ComponentStdioWriterInner::Inherit => {
299+
std::io::stderr().write_all(buf)?;
300+
Ok(buf.len())
301+
}
302+
ComponentStdioWriterInner::Forward {
303+
sync_file, follow, ..
304+
} => {
305+
let written = sync_file.write(buf)?;
306+
if *follow {
307+
std::io::stderr().write_all(&buf[..written])?;
308+
}
309+
Ok(written)
310+
}
242311
}
243-
Ok(written)
244312
}
245313

246314
fn flush(&mut self) -> std::io::Result<()> {
247-
self.sync_file.flush()?;
248-
if self.follow {
249-
std::io::stderr().flush()?;
315+
match &mut self.inner {
316+
ComponentStdioWriterInner::Inherit => std::io::stderr().flush(),
317+
ComponentStdioWriterInner::Forward {
318+
sync_file, follow, ..
319+
} => {
320+
sync_file.flush()?;
321+
if *follow {
322+
std::io::stderr().flush()?;
323+
}
324+
Ok(())
325+
}
250326
}
251-
Ok(())
252327
}
253328
}
254329

examples/spin-timer/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)