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