Skip to main content

spin_telemetry/
logs.rs

1use std::{ascii::escape_default, sync::OnceLock};
2
3use anyhow::bail;
4use opentelemetry::logs::{LogRecord, Logger, LoggerProvider};
5use opentelemetry_otlp::WithHttpConfig;
6use opentelemetry_sdk::{
7    Resource,
8    logs::{BatchConfigBuilder, SdkLogger, log_processor_with_async_runtime::BatchLogProcessor},
9    resource::{EnvResourceDetector, ResourceDetector, TelemetryResourceDetector},
10    runtime::Tokio,
11};
12
13use crate::{
14    detector::SpinResourceDetector,
15    env::{self, OtlpProtocol, otel_logs_enabled},
16};
17
18static LOGGER: OnceLock<SdkLogger> = OnceLock::new();
19
20/// Handle an application log. Has the potential to both forward the log to OTel and to emit it as a
21/// tracing event.
22pub fn handle_app_log(buf: &[u8], component_id: &str) {
23    app_log_to_otel(buf, component_id);
24    app_log_to_tracing_event(buf);
25}
26
27/// Forward the app log to OTel.
28fn app_log_to_otel(buf: &[u8], component_id: &str) {
29    if !otel_logs_enabled() {
30        return;
31    }
32
33    if let Some(logger) = LOGGER.get() {
34        if let Ok(s) = std::str::from_utf8(buf) {
35            let mut record = logger.create_log_record();
36            record.set_body(s.to_string().into());
37            record.add_attribute("component_id", component_id.to_string());
38            logger.emit(record);
39        } else {
40            let mut record = logger.create_log_record();
41            record.set_body(escape_non_utf8_buf(buf).into());
42            record.add_attribute("app_log_non_utf8", true);
43            record.add_attribute("component_id", component_id.to_string());
44            logger.emit(record);
45        }
46    } else {
47        tracing::trace!("OTel logger not initialized, failed to log");
48    }
49}
50
51/// Takes a Spin application log and emits it as a tracing event. This acts as a compatibility layer
52/// to easily get Spin app logs as events in our OTel traces.
53fn app_log_to_tracing_event(buf: &[u8]) {
54    static CELL: OnceLock<bool> = OnceLock::new();
55    if *CELL.get_or_init(env::spin_disable_log_to_tracing) {
56        return;
57    }
58
59    if let Ok(s) = std::str::from_utf8(buf) {
60        tracing::info!(app_log = s);
61    } else {
62        tracing::info!(app_log_non_utf8 = escape_non_utf8_buf(buf));
63    }
64}
65
66fn escape_non_utf8_buf(buf: &[u8]) -> String {
67    buf.iter()
68        .take(50)
69        .map(|&x| escape_default(x).to_string())
70        .collect::<String>()
71}
72
73/// Initialize the OTel logging backend.
74pub(crate) fn init_otel_logging_backend(spin_version: String) -> anyhow::Result<()> {
75    let resource = Resource::builder()
76        .with_detectors(&[
77            // Set service.name from env OTEL_SERVICE_NAME > env OTEL_RESOURCE_ATTRIBUTES > spin
78            // Set service.version from Spin metadata
79            Box::new(SpinResourceDetector::new(spin_version)) as Box<dyn ResourceDetector>,
80            // Sets fields from env OTEL_RESOURCE_ATTRIBUTES
81            Box::new(EnvResourceDetector::new()),
82            // Sets telemetry.sdk{name, language, version}
83            Box::new(TelemetryResourceDetector),
84        ])
85        .build();
86
87    // This will configure the exporter based on the OTEL_EXPORTER_* environment variables. We
88    // currently default to using the HTTP exporter but in the future we could select off of the
89    // combination of OTEL_EXPORTER_OTLP_PROTOCOL and OTEL_EXPORTER_OTLP_LOGS_PROTOCOL to
90    // determine whether we should use http/protobuf or grpc.
91    let exporter = match OtlpProtocol::logs_protocol_from_env() {
92        OtlpProtocol::Grpc => opentelemetry_otlp::LogExporter::builder()
93            .with_tonic()
94            .build()?,
95        OtlpProtocol::HttpProtobuf => opentelemetry_otlp::LogExporter::builder()
96            .with_http()
97            .with_http_client(crate::rustls_reqwest_client()?)
98            .build()?,
99        OtlpProtocol::HttpJson => bail!("http/json OTLP protocol is not supported"),
100    };
101
102    let provider = opentelemetry_sdk::logs::SdkLoggerProvider::builder()
103        .with_resource(resource)
104        .with_log_processor(
105            BatchLogProcessor::builder(exporter, Tokio)
106                .with_batch_config(BatchConfigBuilder::default().build())
107                .build(),
108        )
109        .build();
110
111    let _ = LOGGER.set(provider.logger("spin"));
112    Ok(())
113}