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
20pub 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
27fn 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
51fn 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
73pub(crate) fn init_otel_logging_backend(spin_version: String) -> anyhow::Result<()> {
75 let resource = Resource::builder()
76 .with_detectors(&[
77 Box::new(SpinResourceDetector::new(spin_version)) as Box<dyn ResourceDetector>,
80 Box::new(EnvResourceDetector::new()),
82 Box::new(TelemetryResourceDetector),
84 ])
85 .build();
86
87 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}