diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index 9ade5872..46185d8a 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -43,6 +43,7 @@ body: - opentelemetry-contrib - opentelemetry-datadog - opentelemetry-dynatrace + - opentelemetry-etw-logs - opentelemetry-stackdriver - opentelemetry-user-events-logs - opentelemetry-user-events-metrics diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml index 55599fdf..a622253f 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml +++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml @@ -32,6 +32,7 @@ body: - opentelemetry-contrib - opentelemetry-datadog - opentelemetry-dynatrace + - opentelemetry-etw-logs - opentelemetry-stackdriver - opentelemetry-user-events-logs - opentelemetry-user-events-metrics diff --git a/Cargo.toml b/Cargo.toml index 6a32656c..bc1dbbf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "opentelemetry-aws", "opentelemetry-contrib", "opentelemetry-datadog", + "opentelemetry-etw-logs", "opentelemetry-stackdriver", "opentelemetry-user-events-logs", "opentelemetry-user-events-metrics", diff --git a/opentelemetry-etw-logs/CHANGELOG.md b/opentelemetry-etw-logs/CHANGELOG.md new file mode 100644 index 00000000..95c18e44 --- /dev/null +++ b/opentelemetry-etw-logs/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial Alpha implementation diff --git a/opentelemetry-etw-logs/CODEOWNERS b/opentelemetry-etw-logs/CODEOWNERS new file mode 100644 index 00000000..d6962a90 --- /dev/null +++ b/opentelemetry-etw-logs/CODEOWNERS @@ -0,0 +1,5 @@ +# Code owners file. +# This file controls who is tagged for review for any given pull request. + +# For anything not explicitly taken by someone else: +* @open-telemetry/rust-approvers diff --git a/opentelemetry-etw-logs/Cargo.toml b/opentelemetry-etw-logs/Cargo.toml new file mode 100644 index 00000000..5f1ca73b --- /dev/null +++ b/opentelemetry-etw-logs/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "opentelemetry-etw-logs" +description = "OpenTelemetry logs exporter to ETW (Event Tracing for Windows)" +version = "0.1.0" +edition = "2021" +homepage = "https://github.com/open-telemetry/opentelemetry-rust-contrib/tree/main/opentelemetry-etw-logs" +repository = "https://github.com/open-telemetry/opentelemetry-rust-contrib/tree/main/opentelemetry-etw-logs" +readme = "README.md" +rust-version = "1.65.0" +keywords = ["opentelemetry", "log", "trace", "etw"] +license = "Apache-2.0" + +[dependencies] +tracelogging = "1.2.1" +tracelogging_dynamic = "1.2.1" +opentelemetry = { workspace = true, features = ["logs"] } +opentelemetry_sdk = { workspace = true, features = ["logs"] } +async-trait = { version = "0.1" } +serde_json = "1.0.113" + +[dev-dependencies] +opentelemetry-appender-tracing = { workspace = true } +tracing = { version = "0.1", default-features = false, features = ["std"] } +tracing-core = "0.1.31" +tracing-subscriber = { version = "0.3.0", default-features = false, features = [ + "registry", + "std", +] } +microbench = "0.5" + +[features] +logs_level_enabled = [ + "opentelemetry/logs_level_enabled", + "opentelemetry_sdk/logs_level_enabled", +] +default = ["logs_level_enabled"] + +[[example]] +name = "basic" +path = "examples/basic.rs" diff --git a/opentelemetry-etw-logs/LICENSE b/opentelemetry-etw-logs/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/opentelemetry-etw-logs/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/opentelemetry-etw-logs/README.md b/opentelemetry-etw-logs/README.md new file mode 100644 index 00000000..4f8f493d --- /dev/null +++ b/opentelemetry-etw-logs/README.md @@ -0,0 +1,10 @@ +![OpenTelemetry — An observability framework for cloud-native software.][splash] + +[splash]: https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo-text.png + +# OpenTelemetry ETW Exporter + +## Overview + + ETW (Event Tracing for Windows) is a Windows solution for user process tracing (https://learn.microsoft.com/en-us/windows-hardware/drivers/devtest/event-tracing-for-windows--etw-), similar to [user_events](https://docs.kernel.org/trace/user_events.html) on Linux. + diff --git a/opentelemetry-etw-logs/examples/basic.rs b/opentelemetry-etw-logs/examples/basic.rs new file mode 100644 index 00000000..7d3866cf --- /dev/null +++ b/opentelemetry-etw-logs/examples/basic.rs @@ -0,0 +1,76 @@ +//! Basic example of logs instrumentation with the opentelemetry-etw-logs crate. + +//! This example demonstrates how to use the opentelemetry-etw-logs crate with the tracing crate. +//! +//! run with `$ cargo run --example basic --all-features +//! +//! To view the telemetry emitted to ETW you can use [`logman`](https://learn.microsoft.com/windows-server/administration/windows-commands/logman) along with `perfview`. `logman` will listen to ETW +//! events from the given provider (on this example, `my-provider-name`) and store them in a `.etl` file. +//! [`perfview`](https://github.com/microsoft/perfview) will allow you to visualize the events. +//! +//! Instructions using Powershell: +//! +//! 1. Get the ETW Session Guid for the given provider (on this example `my-provider-name`): +//! ``` +//! $EtwSessionGuid = (new-object System.Diagnostics.Tracing.EventSource("my-provider-name")).Guid.ToString()` +//! ``` +//! 1. Start Logman session: +//! ``` +//! logman create trace OtelETWExampleBasic -o OtelETWExampleBasic.log -p "{$EtwSessionGuid}" -f bincirc -max 1000 +//! logman start OtelETWExampleBasic +//! ``` +//! 1. Execute this example: +//! ``` +//! cd opentelemetry-etw-logs +//! cargo run --example basic +//! ``` +//! 1. Stop and Remove `logman` session: +//! ``` +//! logman stop OtelETWExampleBasic +//! logman delete OtelETWExampleBasic +//! ``` +//! 1. View the events with `perfview`: +//! a. [Download PerView](https://github.com/microsoft/perfview/blob/main/documentation/Downloading.md):[PerfView releases](https://github.com/Microsoft/perfview/releases). +//! a. Open PerfView. +//! a. Go the location of the `.etl` file: `OtelETWExampleBasic.log_000001.etl` and open it. +//! a. Double-click `Events` in the left-panel. +//! a. Double-click the `my-provider-name/my-event-name` in the left-panel. +//! a. You should see the events in the right-panel. +//! + +use opentelemetry_appender_tracing::layer; +use opentelemetry_etw_logs::{ExporterConfig, ReentrantLogProcessor}; +use opentelemetry_sdk::logs::LoggerProvider; +use std::collections::HashMap; +use tracing::error; +use tracing_subscriber::prelude::*; + +fn init_logger() -> LoggerProvider { + let exporter_config = ExporterConfig { + default_keyword: 1, + keywords_map: HashMap::new(), + }; + let reenterant_processor = ReentrantLogProcessor::new( + "my-provider-name", + "my-event-name".into(), + None, + exporter_config, + ); + LoggerProvider::builder() + .with_log_processor(reenterant_processor) + .build() +} + +fn main() { + // Example with tracing appender. + let logger_provider = init_logger(); + let layer = layer::OpenTelemetryTracingBridge::new(&logger_provider); + tracing_subscriber::registry().with(layer).init(); + + error!( + name: "my-event-name", + event_id = 20, + user_name = "otel user", + user_email = "otel@opentelemetry.io" + ); +} diff --git a/opentelemetry-etw-logs/src/lib.rs b/opentelemetry-etw-logs/src/lib.rs new file mode 100644 index 00000000..8758c6a9 --- /dev/null +++ b/opentelemetry-etw-logs/src/lib.rs @@ -0,0 +1,8 @@ +//! The ETW exporter will enable applications to use OpenTelemetry API +//! to capture the telemetry events, and write to ETW subsystem. + +#![warn(missing_debug_implementations, missing_docs)] + +mod logs; + +pub use logs::*; diff --git a/opentelemetry-etw-logs/src/logs/converters.rs b/opentelemetry-etw-logs/src/logs/converters.rs new file mode 100644 index 00000000..5116d854 --- /dev/null +++ b/opentelemetry-etw-logs/src/logs/converters.rs @@ -0,0 +1,211 @@ +use opentelemetry::logs::AnyValue; +use opentelemetry::Key; +use serde_json::{json, Map, Value}; +use std::collections::HashMap; + +pub(super) trait IntoJson { + fn as_json_value(&self) -> Value; +} + +impl IntoJson for AnyValue { + fn as_json_value(&self) -> Value { + match &self { + AnyValue::Int(value) => json!(value), + AnyValue::Double(value) => json!(value), + AnyValue::String(value) => json!(value.to_string()), + AnyValue::Boolean(value) => json!(value), + AnyValue::Bytes(_value) => todo!("No support for AnyValue::Bytes yet."), + AnyValue::ListAny(value) => value.as_json_value(), + AnyValue::Map(value) => value.as_json_value(), + } + } +} + +impl IntoJson for HashMap { + fn as_json_value(&self) -> Value { + Value::Object( + self.iter() + .map(|(k, v)| (k.to_string(), v.as_json_value())) + .collect::>(), + ) + } +} + +impl IntoJson for [AnyValue] { + fn as_json_value(&self) -> Value { + Value::Array(self.iter().map(IntoJson::as_json_value).collect()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use opentelemetry::Key; + + #[test] + fn test_convert_vec_of_any_value_to_string() { + let vec = vec![ + AnyValue::Int(1), + AnyValue::Int(2), + AnyValue::Int(3), + AnyValue::Int(0), + AnyValue::Int(-2), + ]; + let result = vec.as_json_value(); + assert_eq!(result, json!([1, 2, 3, 0, -2])); + + let vec = vec![]; + let result = vec.as_json_value(); + assert_eq!(result, json!([])); + + let vec = vec![AnyValue::ListAny(vec![ + AnyValue::Int(1), + AnyValue::Int(2), + AnyValue::Int(3), + ])]; + let result = vec.as_json_value(); + assert_eq!(result, json!([[1, 2, 3]])); + + let vec = vec![ + AnyValue::ListAny(vec![AnyValue::Int(1), AnyValue::Int(2)]), + AnyValue::ListAny(vec![AnyValue::Int(3), AnyValue::Int(4)]), + ]; + let result = vec.as_json_value(); + assert_eq!(result, json!([[1, 2], [3, 4]])); + + let vec = vec![AnyValue::Boolean(true), AnyValue::Boolean(false)]; + let result = vec.as_json_value(); + assert_eq!(result, json!([true, false])); + + let vec = vec![ + AnyValue::Double(1.0), + AnyValue::Double(-1.0), + AnyValue::Double(0.0), + AnyValue::Double(0.1), + AnyValue::Double(-0.5), + ]; + let result = vec.as_json_value(); + assert_eq!(result, json!([1.0, -1.0, 0.0, 0.1, -0.5])); + + let vec = vec![ + AnyValue::String("".into()), + AnyValue::String("a".into()), + AnyValue::String(r#"""#.into()), + AnyValue::String(r#""""#.into()), + AnyValue::String(r#"foo bar"#.into()), + AnyValue::String(r#""foo bar""#.into()), + ]; + let result = vec.as_json_value(); + assert_eq!( + result, + json!(["", "a", "\"", "\"\"", "foo bar", "\"foo bar\""]) + ); + } + + #[test] + #[should_panic] + fn test_convert_bytes_panics() { + let vec = vec![ + AnyValue::Bytes(vec![97u8, 98u8, 99u8]), + AnyValue::Bytes(vec![]), + ]; + let result = vec.as_json_value(); + assert_eq!(result, json!(["abc", ""])); + } + + #[test] + fn test_convert_map_of_any_value_to_string() { + let mut map: HashMap = HashMap::new(); + map.insert(Key::new("a"), AnyValue::Int(1)); + map.insert(Key::new("b"), AnyValue::Int(2)); + map.insert(Key::new("c"), AnyValue::Int(3)); + map.insert(Key::new("d"), AnyValue::Int(0)); + map.insert(Key::new("e"), AnyValue::Int(-2)); + let result = map.as_json_value(); + assert_eq!(result, json!({"a": 1, "b": 2, "c": 3, "d": 0, "e": -2})); + + let map = HashMap::new(); + let result = map.as_json_value(); + assert_eq!(result, json!({})); + + let mut inner_map = HashMap::new(); + inner_map.insert(Key::new("a"), AnyValue::Int(1)); + inner_map.insert(Key::new("b"), AnyValue::Int(2)); + inner_map.insert(Key::new("c"), AnyValue::Int(3)); + let mut map = HashMap::new(); + map.insert(Key::new("d"), AnyValue::Int(4)); + map.insert(Key::new("e"), AnyValue::Int(5)); + map.insert(Key::new("f"), AnyValue::Map(inner_map)); + let result = map.as_json_value(); + assert_eq!(result, json!({"d":4,"e":5,"f":{"a":1,"b":2,"c":3}})); + + let mut map = HashMap::new(); + map.insert(Key::new("True"), AnyValue::Boolean(true)); + map.insert(Key::new("False"), AnyValue::Boolean(false)); + let result = map.as_json_value(); + assert_eq!(result, json!({"True":true,"False":false})); + + let mut map = HashMap::new(); + map.insert(Key::new("a"), AnyValue::Double(1.0)); + map.insert(Key::new("b"), AnyValue::Double(-1.0)); + map.insert(Key::new("c"), AnyValue::Double(0.0)); + map.insert(Key::new("d"), AnyValue::Double(0.1)); + map.insert(Key::new("e"), AnyValue::Double(-0.5)); + let result = map.as_json_value(); + assert_eq!(result, json!({"a":1.0,"b":-1.0,"c":0.0,"d":0.1,"e":-0.5})); + + let mut map = HashMap::new(); + map.insert(Key::new("a"), AnyValue::String("".into())); + map.insert(Key::new("b"), AnyValue::String("a".into())); + map.insert(Key::new("c"), AnyValue::String(r#"""#.into())); + map.insert(Key::new("d"), AnyValue::String(r#""""#.into())); + map.insert(Key::new("e"), AnyValue::String(r#"foo bar"#.into())); + map.insert(Key::new("f"), AnyValue::String(r#""foo bar""#.into())); + map.insert(Key::new(""), AnyValue::String(r#"empty key"#.into())); + map.insert(Key::new(r#"""#), AnyValue::String(r#"quote"#.into())); + map.insert(Key::new(r#""""#), AnyValue::String(r#"quotes"#.into())); + let result = map.as_json_value(); + assert_eq!( + result, + json!({"a":"","b":"a","c":"\"","d":"\"\"","e":"foo bar","f":"\"foo bar\"","":"empty key","\"":"quote","\"\"":"quotes"}) + ); + } + + #[test] + fn test_complex_conversions() { + let mut simple_map = HashMap::new(); + simple_map.insert(Key::new("a"), AnyValue::Int(1)); + simple_map.insert(Key::new("b"), AnyValue::Int(2)); + + let empty_map: HashMap = HashMap::new(); + + let simple_vec = vec![AnyValue::Int(1), AnyValue::Int(2)]; + + let empty_vec = vec![]; + + let mut complex_map = HashMap::new(); + complex_map.insert(Key::new("a"), AnyValue::Map(simple_map.clone())); + complex_map.insert(Key::new("b"), AnyValue::Map(empty_map.clone())); + complex_map.insert(Key::new("c"), AnyValue::ListAny(simple_vec.clone())); + complex_map.insert(Key::new("d"), AnyValue::ListAny(empty_vec.clone())); + let result = complex_map.as_json_value(); + assert_eq!(result, json!({"a":{"a":1,"b":2},"b":{},"c":[1,2],"d":[]})); + + let complex_vec = vec![ + AnyValue::Map(simple_map.clone()), + AnyValue::Map(empty_map.clone()), + AnyValue::ListAny(simple_vec.clone()), + AnyValue::ListAny(empty_vec.clone()), + ]; + let result = complex_vec.as_json_value(); + assert_eq!(result, json!([{"a":1,"b":2},{},[1,2],[]])); + + let mut nested_complex_map = HashMap::new(); + nested_complex_map.insert(Key::new("a"), AnyValue::Map(complex_map.clone())); + let result = nested_complex_map.as_json_value(); + assert_eq!( + result, + json!({"a":{"a":{"a":1,"b":2},"b":{},"c":[1,2],"d":[]}}) + ); + } +} diff --git a/opentelemetry-etw-logs/src/logs/exporter.rs b/opentelemetry-etw-logs/src/logs/exporter.rs new file mode 100644 index 00000000..e6a58259 --- /dev/null +++ b/opentelemetry-etw-logs/src/logs/exporter.rs @@ -0,0 +1,464 @@ +use async_trait::async_trait; +use std::borrow::Cow; +use std::collections::HashMap; +use std::fmt::Debug; +use std::pin::Pin; +use std::sync::Arc; +use tracelogging::win_filetime_from_systemtime; +use tracelogging_dynamic as tld; + +use opentelemetry::{ + logs::{AnyValue, Severity}, + Key, +}; +use std::{str, time::SystemTime}; + +use crate::logs::converters::IntoJson; + +/// Provider group associated with the ETW exporter +pub type ProviderGroup = Option>; + +// thread_local! { static EBW: RefCell = RefCell::new(EventBuilder::new());} + +/// Exporter config +#[derive(Debug)] +pub struct ExporterConfig { + /// keyword associated with ETW name + /// These should be mapped to logger_name as of now. + pub keywords_map: HashMap, + /// default keyword if map is not defined. + pub default_keyword: u64, +} + +impl Default for ExporterConfig { + fn default() -> Self { + ExporterConfig { + keywords_map: HashMap::new(), + default_keyword: 1, + } + } +} + +impl ExporterConfig { + pub(crate) fn get_log_keyword(&self, name: &str) -> Option { + self.keywords_map.get(name).copied() + } + + pub(crate) fn get_log_keyword_or_default(&self, name: &str) -> Option { + if self.keywords_map.is_empty() { + Some(self.default_keyword) + } else { + self.get_log_keyword(name) + } + } +} +pub(crate) struct ETWExporter { + provider: Pin>, + exporter_config: ExporterConfig, + event_name: String, +} + +const EVENT_ID: &str = "event_id"; +const EVENT_NAME_PRIMARY: &str = "event_name"; +const EVENT_NAME_SECONDARY: &str = "name"; + +// TODO: Implement callback +fn enabled_callback( + _source_id: &tld::Guid, + _event_control_code: u32, + _level: tld::Level, + _match_any_keyword: u64, + _match_all_keyword: u64, + _filter_data: usize, + _callback_context: usize, +) { +} + +//TBD - How to configure provider name and provider group +impl ETWExporter { + pub(crate) fn new( + provider_name: &str, + event_name: String, + _provider_group: ProviderGroup, + exporter_config: ExporterConfig, + ) -> Self { + let mut options = tld::Provider::options(); + // TODO: Implement callback + options.callback(enabled_callback, 0x0); + let provider = Arc::pin(tld::Provider::new(provider_name, &options)); + // SAFETY: tracelogging (ETW) enables an ETW callback into the provider when `register()` is called. + // This might crash if the provider is dropped without calling unregister before. + // This only affects static providers. + // On dynamically created providers, the lifetime of the provider is tied to the object itself, so `unregister()` is called when dropped. + unsafe { + provider.as_ref().register(); + } + // TODO: enable keywords on callback + // Self::register_keywords(&mut provider, &exporter_config); + ETWExporter { + provider, + exporter_config, + event_name, + } + } + + // TODO: enable keywords on callback + // fn register_events(provider: &mut tld::Provider, keyword: u64) { + // let levels = [ + // tld::Level::Verbose, + // tld::Level::Informational, + // tld::Level::Warning, + // tld::Level::Error, + // tld::Level::Critical, + // tld::Level::LogAlways, + // ]; + + // for &level in levels.iter() { + // // provider.register_set(level, keyword); + // } + // } + + // fn register_keywords(provider: &mut tld::Provider, exporter_config: &ExporterConfig) { + // if exporter_config.keywords_map.is_empty() { + // println!( + // "Register default keyword {}", + // exporter_config.default_keyword + // ); + // Self::register_events(provider, exporter_config.default_keyword); + // } + + // for keyword in exporter_config.keywords_map.values() { + // Self::register_events(provider, *keyword); + // } + // } + + fn get_severity_level(&self, severity: Severity) -> tld::Level { + match severity { + Severity::Debug + | Severity::Debug2 + | Severity::Debug3 + | Severity::Debug4 + | Severity::Trace + | Severity::Trace2 + | Severity::Trace3 + | Severity::Trace4 => tld::Level::Verbose, + + Severity::Info | Severity::Info2 | Severity::Info3 | Severity::Info4 => { + tld::Level::Informational + } + + Severity::Error | Severity::Error2 | Severity::Error3 | Severity::Error4 => { + tld::Level::Error + } + + Severity::Fatal | Severity::Fatal2 | Severity::Fatal3 | Severity::Fatal4 => { + tld::Level::Critical + } + + Severity::Warn | Severity::Warn2 | Severity::Warn3 | Severity::Warn4 => { + tld::Level::Warning + } + } + } + + #[allow(dead_code)] + fn enabled(&self, level: u8, keyword: u64) -> bool { + // TODO: Use internal enabled check. Meaning of enable differs from OpenTelemetry and ETW. + // OpenTelemetry wants to know if level+keyword combination is enabled for the Provider. + // ETW tells if level+keyword combination is being actively listened. Not all systems actively + // listens for ETW events, but they do it on samples. + // This may be fixed by applying the OpenTelemetry logic in the callback function. + self.provider.enabled(level.into(), keyword) + } + + pub(crate) fn export_log_data( + &self, + log_data: &opentelemetry_sdk::export::logs::LogData, + ) -> opentelemetry_sdk::export::logs::ExportResult { + let level = + self.get_severity_level(log_data.record.severity_number.unwrap_or(Severity::Debug)); + + let keyword = match self + .exporter_config + .get_log_keyword_or_default(log_data.instrumentation.name.as_ref()) + { + Some(keyword) => keyword, + _ => return Ok(()), + }; + + if !self.provider.enabled(level.as_int().into(), keyword) { + return Ok(()); + }; + + let event_tags: u32 = 0; // TBD name and event_tag values + let field_tag: u32 = 0; + let mut event = tld::EventBuilder::new(); + + // reset + event.reset(&self.event_name, level, keyword, event_tags); + + event.add_u16("__csver__", 0x0401u16, tld::OutType::Hex, field_tag); + + self.populate_part_a(&mut event, log_data, field_tag); + + let (event_id, event_name) = self.populate_part_c(&mut event, log_data, field_tag); + + self.populate_part_b(&mut event, log_data, level, event_id, event_name); + + // Write event to ETW + let result = event.write(&self.provider, None, None); + + match result { + 0 => Ok(()), + _ => Err(format!("Failed to write event to ETW. ETW reason: {result}").into()), + } + } + + fn populate_part_a( + &self, + event: &mut tld::EventBuilder, + log_data: &opentelemetry_sdk::export::logs::LogData, + field_tag: u32, + ) { + let event_time: SystemTime = log_data + .record + .timestamp + .unwrap_or(log_data.record.observed_timestamp); + + const COUNT_TIME: u8 = 1u8; + const PART_A_COUNT: u8 = COUNT_TIME; + event.add_struct("PartA", PART_A_COUNT, field_tag); + { + let timestamp = win_filetime_from_systemtime!(event_time); + event.add_filetime("time", timestamp, tld::OutType::Default, field_tag); + } + } + + fn populate_part_b( + &self, + event: &mut tld::EventBuilder, + log_data: &opentelemetry_sdk::export::logs::LogData, + level: tld::Level, + event_id: Option, + event_name: Option<&str>, + ) { + // Count fields in PartB + const COUNT_TYPE_NAME: u8 = 1u8; + const COUNT_SEVERITY_NUMBER: u8 = 1u8; + + let field_count = COUNT_TYPE_NAME + + COUNT_SEVERITY_NUMBER + + log_data.record.body.is_some() as u8 + + log_data.record.severity_text.is_some() as u8 + + event_id.is_some() as u8 + + event_name.is_some() as u8; + + // Create PartB struct + event.add_struct("PartB", field_count, 0); + + // Fill fields of PartB struct + event.add_str8("_typeName", "Logs", tld::OutType::Default, 0); + + if let Some(body) = log_data.record.body.clone() { + add_attribute_to_event(event, &Key::new("body"), &body); + } + + event.add_u8("severityNumber", level.as_int(), tld::OutType::Default, 0); + + if let Some(severity_text) = &log_data.record.severity_text { + event.add_str8( + "severityText", + severity_text.as_ref(), + tld::OutType::Default, + 0, + ); + } + + if let Some(event_id) = event_id { + event.add_i64("eventId", event_id, tld::OutType::Default, 0); + } + + if let Some(event_name) = event_name { + event.add_str8("name", event_name, tld::OutType::Default, 0); + } + } + + fn populate_part_c<'a>( + &'a self, + event: &mut tld::EventBuilder, + log_data: &'a opentelemetry_sdk::export::logs::LogData, + field_tag: u32, + ) -> (Option, Option<&str>) { + //populate CS PartC + let mut event_id: Option = None; + let mut event_name: Option<&str> = None; + + if let Some(attr_list) = &log_data.record.attributes { + let mut cs_c_count = 0; + + // find if we have PartC and its information + for (key, value) in attr_list.iter() { + match (key.as_str(), &value) { + (EVENT_ID, AnyValue::Int(value)) => { + event_id = Some(*value); + continue; + } + (EVENT_NAME_PRIMARY, AnyValue::String(value)) => { + event_name = Some(value.as_str()); + continue; + } + (EVENT_NAME_SECONDARY, AnyValue::String(value)) => { + if event_name.is_none() { + event_name = Some(value.as_str()); + } + continue; + } + _ => { + cs_c_count += 1; + } + } + } + + if cs_c_count > 0 { + event.add_struct("PartC", cs_c_count, field_tag); + + for (key, value) in attr_list.iter() { + match (key.as_str(), &value) { + (EVENT_ID, _) | (EVENT_NAME_PRIMARY, _) | (EVENT_NAME_SECONDARY, _) => { + continue; + } + _ => { + add_attribute_to_event(event, key, value); + } + } + } + } + } + + (event_id, event_name) + } +} + +impl Debug for ETWExporter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("ETW log exporter") + } +} + +#[async_trait] +impl opentelemetry_sdk::export::logs::LogExporter for ETWExporter { + async fn export( + &mut self, + batch: Vec, + ) -> opentelemetry::logs::LogResult<()> { + for log_data in batch { + let _ = self.export_log_data(&log_data); + } + Ok(()) + } + + #[cfg(feature = "logs_level_enabled")] + fn event_enabled(&self, level: Severity, _target: &str, name: &str) -> bool { + let (found, keyword) = if self.exporter_config.keywords_map.is_empty() { + (true, self.exporter_config.default_keyword) + } else { + // TBD - target is not used as of now for comparison. + match self.exporter_config.get_log_keyword(name) { + Some(x) => (true, x), + _ => (false, 0), + } + }; + if !found { + return false; + } + self.provider + .enabled(self.get_severity_level(level), keyword) + } +} + +fn add_attribute_to_event(event: &mut tld::EventBuilder, key: &Key, value: &AnyValue) { + match value { + AnyValue::Boolean(b) => { + event.add_bool32(key.as_str(), *b as i32, tld::OutType::Default, 0); + } + AnyValue::Int(i) => { + event.add_i64(key.as_str(), *i, tld::OutType::Default, 0); + } + AnyValue::Double(f) => { + event.add_f64(key.as_str(), *f, tld::OutType::Default, 0); + } + AnyValue::String(s) => { + event.add_str8(key.as_str(), s.as_str(), tld::OutType::Default, 0); + } + AnyValue::Bytes(b) => { + event.add_binaryc(key.as_str(), b.as_slice(), tld::OutType::Default, 0); + } + AnyValue::ListAny(l) => { + event.add_str8( + key.as_str(), + &l.as_json_value().to_string(), + tld::OutType::Json, + 0, + ); + } + AnyValue::Map(m) => { + event.add_str8( + key.as_str(), + &m.as_json_value().to_string(), + tld::OutType::Json, + 0, + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use opentelemetry::logs::Severity; + use opentelemetry_sdk::export::logs::LogData; + + #[test] + fn test_export_log_data() { + let exporter = ETWExporter::new( + "test-provider-name", + "test-event-name".to_string(), + None, + ExporterConfig::default(), + ); + + let log_data = LogData { + instrumentation: Default::default(), + record: Default::default(), + resource: Default::default(), + }; + + let result = exporter.export_log_data(&log_data); + assert!(result.is_ok()); + } + + #[test] + fn test_get_severity_level() { + let exporter = ETWExporter::new( + "test-provider-name", + "test-event-name".to_string(), + None, + ExporterConfig::default(), + ); + + let result = exporter.get_severity_level(Severity::Debug); + assert_eq!(result, tld::Level::Verbose); + + let result = exporter.get_severity_level(Severity::Info); + assert_eq!(result, tld::Level::Informational); + + let result = exporter.get_severity_level(Severity::Error); + assert_eq!(result, tld::Level::Error); + + let result = exporter.get_severity_level(Severity::Fatal); + assert_eq!(result, tld::Level::Critical); + + let result = exporter.get_severity_level(Severity::Warn); + assert_eq!(result, tld::Level::Warning); + } +} diff --git a/opentelemetry-etw-logs/src/logs/mod.rs b/opentelemetry-etw-logs/src/logs/mod.rs new file mode 100644 index 00000000..7233892e --- /dev/null +++ b/opentelemetry-etw-logs/src/logs/mod.rs @@ -0,0 +1,7 @@ +mod exporter; +pub use exporter::*; + +mod reentrant_logprocessor; +pub use reentrant_logprocessor::*; + +mod converters; diff --git a/opentelemetry-etw-logs/src/logs/reentrant_logprocessor.rs b/opentelemetry-etw-logs/src/logs/reentrant_logprocessor.rs new file mode 100644 index 00000000..9f85e5e2 --- /dev/null +++ b/opentelemetry-etw-logs/src/logs/reentrant_logprocessor.rs @@ -0,0 +1,108 @@ +use std::fmt::Debug; + +use opentelemetry::logs::LogResult; +use opentelemetry_sdk::export::logs::LogData; + +#[cfg(feature = "logs_level_enabled")] +use opentelemetry_sdk::export::logs::LogExporter; + +use crate::logs::exporter::ExporterConfig; +use crate::logs::exporter::*; + +/// Thread-safe LogProcessor for exporting logs to ETW. + +#[derive(Debug)] +pub struct ReentrantLogProcessor { + event_exporter: ETWExporter, +} + +impl ReentrantLogProcessor { + /// constructor + pub fn new( + provider_name: &str, + event_name: String, + provider_group: ProviderGroup, + exporter_config: ExporterConfig, + ) -> Self { + let exporter = ETWExporter::new(provider_name, event_name, provider_group, exporter_config); + ReentrantLogProcessor { + event_exporter: exporter, + } + } +} + +impl opentelemetry_sdk::logs::LogProcessor for ReentrantLogProcessor { + fn emit(&self, data: LogData) { + _ = self.event_exporter.export_log_data(&data); + } + + // This is a no-op as this processor doesn't keep anything + // in memory to be flushed out. + fn force_flush(&self) -> LogResult<()> { + Ok(()) + } + + // This is a no-op no special cleanup is required before + // shutdown. + fn shutdown(&mut self) -> LogResult<()> { + Ok(()) + } + + #[cfg(feature = "logs_level_enabled")] + fn event_enabled( + &self, + level: opentelemetry::logs::Severity, + target: &str, + name: &str, + ) -> bool { + self.event_exporter.event_enabled(level, target, name) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use opentelemetry_sdk::logs::LogProcessor; + + #[test] + fn test_shutdown() { + let mut processor = ReentrantLogProcessor::new( + "test-provider-name", + "test-event-name".into(), + None, + ExporterConfig::default(), + ); + + assert!(processor.shutdown().is_ok()); + } + + #[test] + fn test_force_flush() { + let processor = ReentrantLogProcessor::new( + "test-provider-name", + "test-event-name".into(), + None, + ExporterConfig::default(), + ); + + assert!(processor.force_flush().is_ok()); + } + + #[test] + fn test_emit() { + let processor = ReentrantLogProcessor::new( + "test-provider-name", + "test-event-name".into(), + None, + ExporterConfig::default(), + ); + + let log_data = LogData { + instrumentation: Default::default(), + record: Default::default(), + resource: Default::default(), + }; + + processor.emit(log_data); + } +}