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
19pub fn handle_app_log(buf: &[u8]) {
22 app_log_to_otel(buf);
23 app_log_to_tracing_event(buf);
24}
25
26fn 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
48fn 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
70pub(crate) fn init_otel_logging_backend(spin_version: String) -> anyhow::Result<()> {
72 let resource = Resource::builder()
73 .with_detectors(&[
74 Box::new(SpinResourceDetector::new(spin_version)) as Box<dyn ResourceDetector>,
77 Box::new(EnvResourceDetector::new()),
79 Box::new(TelemetryResourceDetector),
81 ])
82 .build();
83
84 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}