opentelemetry_proto/transform/
logs.rs

1#[cfg(feature = "gen-tonic-messages")]
2pub mod tonic {
3    use crate::{
4        tonic::{
5            common::v1::{
6                any_value::Value, AnyValue, ArrayValue, InstrumentationScope, KeyValue,
7                KeyValueList,
8            },
9            logs::v1::{LogRecord, ResourceLogs, ScopeLogs, SeverityNumber},
10            resource::v1::Resource,
11            Attributes,
12        },
13        transform::common::{to_nanos, tonic::ResourceAttributesWithSchema},
14    };
15    use opentelemetry::logs::{AnyValue as LogsAnyValue, Severity};
16    use std::borrow::Cow;
17    use std::collections::HashMap;
18
19    impl From<LogsAnyValue> for AnyValue {
20        fn from(value: LogsAnyValue) -> Self {
21            AnyValue {
22                value: Some(value.into()),
23            }
24        }
25    }
26
27    impl From<LogsAnyValue> for Value {
28        fn from(value: LogsAnyValue) -> Self {
29            match value {
30                LogsAnyValue::Double(f) => Value::DoubleValue(f),
31                LogsAnyValue::Int(i) => Value::IntValue(i),
32                LogsAnyValue::String(s) => Value::StringValue(s.into()),
33                LogsAnyValue::Boolean(b) => Value::BoolValue(b),
34                LogsAnyValue::ListAny(v) => Value::ArrayValue(ArrayValue {
35                    values: v
36                        .into_iter()
37                        .map(|v| AnyValue {
38                            value: Some(v.into()),
39                        })
40                        .collect(),
41                }),
42                LogsAnyValue::Map(m) => Value::KvlistValue(KeyValueList {
43                    values: m
44                        .into_iter()
45                        .map(|(key, value)| KeyValue {
46                            key: key.into(),
47                            value: Some(AnyValue {
48                                value: Some(value.into()),
49                            }),
50                        })
51                        .collect(),
52                }),
53                LogsAnyValue::Bytes(v) => Value::BytesValue(v),
54            }
55        }
56    }
57
58    impl From<opentelemetry_sdk::logs::LogRecord> for LogRecord {
59        fn from(log_record: opentelemetry_sdk::logs::LogRecord) -> Self {
60            let trace_context = log_record.trace_context.as_ref();
61            let severity_number = match log_record.severity_number {
62                Some(Severity::Trace) => SeverityNumber::Trace,
63                Some(Severity::Trace2) => SeverityNumber::Trace2,
64                Some(Severity::Trace3) => SeverityNumber::Trace3,
65                Some(Severity::Trace4) => SeverityNumber::Trace4,
66                Some(Severity::Debug) => SeverityNumber::Debug,
67                Some(Severity::Debug2) => SeverityNumber::Debug2,
68                Some(Severity::Debug3) => SeverityNumber::Debug3,
69                Some(Severity::Debug4) => SeverityNumber::Debug4,
70                Some(Severity::Info) => SeverityNumber::Info,
71                Some(Severity::Info2) => SeverityNumber::Info2,
72                Some(Severity::Info3) => SeverityNumber::Info3,
73                Some(Severity::Info4) => SeverityNumber::Info4,
74                Some(Severity::Warn) => SeverityNumber::Warn,
75                Some(Severity::Warn2) => SeverityNumber::Warn2,
76                Some(Severity::Warn3) => SeverityNumber::Warn3,
77                Some(Severity::Warn4) => SeverityNumber::Warn4,
78                Some(Severity::Error) => SeverityNumber::Error,
79                Some(Severity::Error2) => SeverityNumber::Error2,
80                Some(Severity::Error3) => SeverityNumber::Error3,
81                Some(Severity::Error4) => SeverityNumber::Error4,
82                Some(Severity::Fatal) => SeverityNumber::Fatal,
83                Some(Severity::Fatal2) => SeverityNumber::Fatal2,
84                Some(Severity::Fatal3) => SeverityNumber::Fatal3,
85                Some(Severity::Fatal4) => SeverityNumber::Fatal4,
86                None => SeverityNumber::Unspecified,
87            };
88
89            LogRecord {
90                time_unix_nano: log_record.timestamp.map(to_nanos).unwrap_or_default(),
91                observed_time_unix_nano: to_nanos(log_record.observed_timestamp.unwrap()),
92                severity_number: severity_number.into(),
93                severity_text: log_record.severity_text.map(Into::into).unwrap_or_default(),
94                body: log_record.body.map(Into::into),
95                attributes: {
96                    let mut attributes = log_record
97                        .attributes
98                        .map(Attributes::from_iter)
99                        .unwrap_or_default()
100                        .0;
101                    if let Some(event_name) = log_record.event_name.as_ref() {
102                        attributes.push(KeyValue {
103                            key: "name".into(),
104                            value: Some(AnyValue {
105                                value: Some(Value::StringValue(event_name.to_string())),
106                            }),
107                        })
108                    }
109                    attributes
110                },
111                dropped_attributes_count: 0,
112                flags: trace_context
113                    .map(|ctx| {
114                        ctx.trace_flags
115                            .map(|flags| flags.to_u8() as u32)
116                            .unwrap_or_default()
117                    })
118                    .unwrap_or_default(),
119                span_id: trace_context
120                    .map(|ctx| ctx.span_id.to_bytes().to_vec())
121                    .unwrap_or_default(),
122                trace_id: trace_context
123                    .map(|ctx| ctx.trace_id.to_bytes().to_vec())
124                    .unwrap_or_default(),
125            }
126        }
127    }
128
129    impl
130        From<(
131            opentelemetry_sdk::export::logs::LogData,
132            &ResourceAttributesWithSchema,
133        )> for ResourceLogs
134    {
135        fn from(
136            data: (
137                opentelemetry_sdk::export::logs::LogData,
138                &ResourceAttributesWithSchema,
139            ),
140        ) -> Self {
141            let (log_data, resource) = data;
142
143            ResourceLogs {
144                resource: Some(Resource {
145                    attributes: resource.attributes.0.clone(),
146                    dropped_attributes_count: 0,
147                }),
148                schema_url: resource.schema_url.clone().unwrap_or_default(),
149                scope_logs: vec![ScopeLogs {
150                    schema_url: log_data
151                        .instrumentation
152                        .schema_url
153                        .clone()
154                        .map(Into::into)
155                        .unwrap_or_default(),
156                    scope: Some((log_data.instrumentation, log_data.record.target.clone()).into()),
157                    log_records: vec![log_data.record.into()],
158                }],
159            }
160        }
161    }
162
163    pub fn group_logs_by_resource_and_scope(
164        logs: Vec<opentelemetry_sdk::export::logs::LogData>,
165        resource: &ResourceAttributesWithSchema,
166    ) -> Vec<ResourceLogs> {
167        // Group logs by target or instrumentation name
168        let scope_map = logs.iter().fold(
169            HashMap::new(),
170            |mut scope_map: HashMap<
171                Cow<'static, str>,
172                Vec<&opentelemetry_sdk::export::logs::LogData>,
173            >,
174             log| {
175                let key = log
176                    .record
177                    .target
178                    .clone()
179                    .unwrap_or_else(|| log.instrumentation.name.clone());
180                scope_map.entry(key).or_default().push(log);
181                scope_map
182            },
183        );
184
185        let scope_logs = scope_map
186            .into_iter()
187            .map(|(key, log_data)| ScopeLogs {
188                scope: Some(InstrumentationScope::from((
189                    &log_data.first().unwrap().instrumentation,
190                    Some(key),
191                ))),
192                schema_url: resource.schema_url.clone().unwrap_or_default(),
193                log_records: log_data
194                    .into_iter()
195                    .map(|log_data| log_data.record.clone().into())
196                    .collect(),
197            })
198            .collect();
199
200        vec![ResourceLogs {
201            resource: Some(Resource {
202                attributes: resource.attributes.0.clone(),
203                dropped_attributes_count: 0,
204            }),
205            scope_logs,
206            schema_url: resource.schema_url.clone().unwrap_or_default(),
207        }]
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use crate::transform::common::tonic::ResourceAttributesWithSchema;
214    use opentelemetry::logs::LogRecord as _;
215    use opentelemetry_sdk::export::logs::LogData;
216    use opentelemetry_sdk::{logs::LogRecord, Resource};
217    use std::time::SystemTime;
218
219    fn create_test_log_data(instrumentation_name: &str, _message: &str) -> LogData {
220        let mut logrecord = LogRecord::default();
221        logrecord.set_timestamp(SystemTime::now());
222        logrecord.set_observed_timestamp(SystemTime::now());
223        LogData {
224            instrumentation: opentelemetry_sdk::InstrumentationLibrary::builder(
225                instrumentation_name.to_string(),
226            )
227            .build(),
228            record: logrecord,
229        }
230    }
231
232    #[test]
233    fn test_group_logs_by_resource_and_scope_single_scope() {
234        let resource = Resource::default();
235        let log1 = create_test_log_data("test-lib", "Log 1");
236        let log2 = create_test_log_data("test-lib", "Log 2");
237
238        let logs = vec![log1, log2];
239        let resource: ResourceAttributesWithSchema = (&resource).into(); // Convert Resource to ResourceAttributesWithSchema
240
241        let grouped_logs =
242            crate::transform::logs::tonic::group_logs_by_resource_and_scope(logs, &resource);
243
244        assert_eq!(grouped_logs.len(), 1);
245        let resource_logs = &grouped_logs[0];
246        assert_eq!(resource_logs.scope_logs.len(), 1);
247
248        let scope_logs = &resource_logs.scope_logs[0];
249        assert_eq!(scope_logs.log_records.len(), 2);
250    }
251
252    #[test]
253    fn test_group_logs_by_resource_and_scope_multiple_scopes() {
254        let resource = Resource::default();
255        let log1 = create_test_log_data("lib1", "Log 1");
256        let log2 = create_test_log_data("lib2", "Log 2");
257
258        let logs = vec![log1, log2];
259        let resource: ResourceAttributesWithSchema = (&resource).into(); // Convert Resource to ResourceAttributesWithSchema
260        let grouped_logs =
261            crate::transform::logs::tonic::group_logs_by_resource_and_scope(logs, &resource);
262
263        assert_eq!(grouped_logs.len(), 1);
264        let resource_logs = &grouped_logs[0];
265        assert_eq!(resource_logs.scope_logs.len(), 2);
266
267        let scope_logs_1 = &resource_logs
268            .scope_logs
269            .iter()
270            .find(|scope| scope.scope.as_ref().unwrap().name == "lib1")
271            .unwrap();
272        let scope_logs_2 = &resource_logs
273            .scope_logs
274            .iter()
275            .find(|scope| scope.scope.as_ref().unwrap().name == "lib2")
276            .unwrap();
277
278        assert_eq!(scope_logs_1.log_records.len(), 1);
279        assert_eq!(scope_logs_2.log_records.len(), 1);
280    }
281}