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