diff --git a/Cargo.lock b/Cargo.lock index adb4bb00c..80f251607 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -749,6 +749,7 @@ dependencies = [ "crossterm", "ctrlc", "dsc-lib", + "dsc-lib-telemetry", "indicatif", "jsonschema", "path-absolutize", @@ -908,6 +909,16 @@ dependencies = [ "nix", ] +[[package]] +name = "dsc-lib-telemetry" +version = "0.0.0" +dependencies = [ + "thiserror 2.0.19", + "tracing", + "tracing-indicatif", + "tracing-subscriber", +] + [[package]] name = "dsc-resource-echo" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index c623cbf4d..65eccb184 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,29 +5,30 @@ resolver = "2" members = [ "dsc", "dsc-bicep-ext", - "lib/dsc-lib", - "lib/dsc-lib-jsonschema", + "grammars/tree-sitter-dscexpression", + "grammars/tree-sitter-ssh-server-config", "lib/dsc-lib-jsonschema-macros", - "resources/dscecho", + "lib/dsc-lib-jsonschema", "lib/dsc-lib-osinfo", - "resources/osinfo", "lib/dsc-lib-pal", - "resources/process", - "resources/registry", "lib/dsc-lib-registry", - "resources/runcommandonset", "lib/dsc-lib-security_context", + "lib/dsc-lib-telemetry", + "lib/dsc-lib", "resources/dism_dsc", + "resources/dscecho", + "resources/osinfo", + "resources/process", + "resources/registry", + "resources/runcommandonset", "resources/sshdconfig", - "resources/WindowsUpdate", - "resources/windows_service", "resources/windows_firewall", + "resources/windows_service", + "resources/WindowsUpdate", "tools/dsctest", "tools/test_group_resource", - "grammars/tree-sitter-dscexpression", - "grammars/tree-sitter-ssh-server-config", - "y2j", - "xtask" + "xtask", + "y2j" ] # This value is modified by the `Set-DefaultWorkspaceMember` helper. @@ -36,29 +37,30 @@ members = [ default-members = [ "dsc", "dsc-bicep-ext", - "lib/dsc-lib", - "lib/dsc-lib-jsonschema", + "grammars/tree-sitter-dscexpression", + "grammars/tree-sitter-ssh-server-config", "lib/dsc-lib-jsonschema-macros", - "resources/dscecho", + "lib/dsc-lib-jsonschema", "lib/dsc-lib-osinfo", - "resources/osinfo", "lib/dsc-lib-pal", - "resources/process", - "resources/registry", "lib/dsc-lib-registry", - "resources/runcommandonset", "lib/dsc-lib-security_context", + "lib/dsc-lib-telemetry", + "lib/dsc-lib", "resources/dism_dsc", + "resources/dscecho", + "resources/osinfo", + "resources/process", + "resources/registry", + "resources/runcommandonset", "resources/sshdconfig", - "resources/WindowsUpdate", - "resources/windows_service", "resources/windows_firewall", + "resources/windows_service", + "resources/WindowsUpdate", "tools/dsctest", "tools/test_group_resource", - "grammars/tree-sitter-dscexpression", - "grammars/tree-sitter-ssh-server-config", - "y2j", - "xtask" + "xtask", + "y2j" ] [workspace.metadata.groups] @@ -68,69 +70,72 @@ default-members = [ Windows = [ "dsc", "dsc-bicep-ext", - "lib/dsc-lib", - "lib/dsc-lib-jsonschema", + "grammars/tree-sitter-dscexpression", + "grammars/tree-sitter-ssh-server-config", "lib/dsc-lib-jsonschema-macros", - "resources/dscecho", + "lib/dsc-lib-jsonschema", "lib/dsc-lib-osinfo", - "resources/osinfo", "lib/dsc-lib-pal", + "lib/dsc-lib-registry", + "lib/dsc-lib-security_context", + "lib/dsc-lib-telemetry", + "lib/dsc-lib", + "resources/dism_dsc", + "resources/dscecho", + "resources/osinfo", "resources/process", "resources/registry", - "lib/dsc-lib-registry", "resources/runcommandonset", - "lib/dsc-lib-security_context", "resources/sshdconfig", - "resources/WindowsUpdate", - "resources/windows_service", "resources/windows_firewall", + "resources/windows_service", + "resources/WindowsUpdate", "tools/dsctest", "tools/test_group_resource", - "grammars/tree-sitter-dscexpression", - "grammars/tree-sitter-ssh-server-config", - "y2j", "xtask", - "resources/dism_dsc" + "y2j" ] macOS = [ "dsc", "dsc-bicep-ext", - "lib/dsc-lib", - "lib/dsc-lib-jsonschema", + "grammars/tree-sitter-dscexpression", + "grammars/tree-sitter-ssh-server-config", "lib/dsc-lib-jsonschema-macros", - "resources/dscecho", + "lib/dsc-lib-jsonschema", "lib/dsc-lib-osinfo", + "lib/dsc-lib-security_context", + "lib/dsc-lib-telemetry", + "lib/dsc-lib", + "resources/dscecho", "resources/osinfo", "resources/process", "resources/runcommandonset", - "lib/dsc-lib-security_context", "resources/sshdconfig", "tools/dsctest", "tools/test_group_resource", - "grammars/tree-sitter-dscexpression", - "grammars/tree-sitter-ssh-server-config", + "xtask", "y2j", - "xtask" ] Linux = [ "dsc", "dsc-bicep-ext", - "lib/dsc-lib", - "lib/dsc-lib-jsonschema", + "grammars/tree-sitter-dscexpression", + "grammars/tree-sitter-ssh-server-config", "lib/dsc-lib-jsonschema-macros", - "resources/dscecho", + "lib/dsc-lib-jsonschema", "lib/dsc-lib-osinfo", + "lib/dsc-lib-security_context", + "lib/dsc-lib-telemetry", + "lib/dsc-lib", + "resources/dscecho", "resources/osinfo", "resources/process", "resources/runcommandonset", - "lib/dsc-lib-security_context", "resources/sshdconfig", "tools/dsctest", "tools/test_group_resource", - "grammars/tree-sitter-dscexpression", - "grammars/tree-sitter-ssh-server-config", - "y2j", - "xtask" + "xtask", + "y2j" ] [profile.release] @@ -299,7 +304,8 @@ dsc-lib = { path = "lib/dsc-lib" } dsc-lib-jsonschema = { path = "lib/dsc-lib-jsonschema" } dsc-lib-jsonschema-macros = { path = "lib/dsc-lib-jsonschema-macros" } dsc-lib-osinfo = { path = "lib/dsc-lib-osinfo" } +dsc-lib-registry = { path = "lib/dsc-lib-registry" } dsc-lib-security_context = { path = "lib/dsc-lib-security_context" } +dsc-lib-telemetry = { path = "lib/dsc-lib-telemetry" } tree-sitter-dscexpression = { path = "grammars/tree-sitter-dscexpression" } -dsc-lib-registry = { path = "lib/dsc-lib-registry" } tree-sitter-ssh-server-config = { path = "grammars/tree-sitter-ssh-server-config" } diff --git a/dsc/Cargo.toml b/dsc/Cargo.toml index a6530d729..9aa836dfe 100644 --- a/dsc/Cargo.toml +++ b/dsc/Cargo.toml @@ -37,3 +37,4 @@ tracing-subscriber = { workspace = true } tracing-indicatif = { workspace = true } # workspace crate dependencies dsc-lib = { workspace = true } +dsc-lib-telemetry = { workspace = true } diff --git a/dsc/src/util.rs b/dsc/src/util.rs index 51eb4596a..4b18fac6c 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -46,6 +46,7 @@ use dsc_lib::{ parse_input_to_json, }, }; +use dsc_lib_telemetry::{StderrFormat, basic::{BasicTracingOptions}}; use path_absolutize::Absolutize; use rust_i18n::t; use schemars::{Schema, schema_for}; @@ -62,8 +63,6 @@ use syntect::{ util::{as_24_bit_terminal_escaped, LinesWithEndings} }; use tracing::{Level, debug, error, info, warn, trace}; -use tracing_subscriber::{filter::EnvFilter, layer::SubscriberExt, Layer}; -use tracing_indicatif::IndicatifLayer; pub const EXIT_SUCCESS: i32 = 0; pub const EXIT_INVALID_ARGS: i32 = 1; @@ -327,22 +326,10 @@ pub fn write_object(json: &str, format: Option<&OutputFormat>, include_separator #[allow(clippy::too_many_lines)] pub fn enable_tracing(trace_level_arg: Option<&TraceLevel>, trace_format_arg: Option<&TraceFormat>) { - let mut policy_is_used = false; let mut tracing_setting = TracingSetting::default(); - let default_filter = EnvFilter::try_from_default_env() - .or_else(|_| EnvFilter::try_new("warn")) - .unwrap_or_default() - .add_directive(Level::WARN.into()); - let default_indicatif_layer = IndicatifLayer::new(); - let default_layer = tracing_subscriber::fmt::Layer::default().with_writer(default_indicatif_layer.get_stderr_writer()); - let default_fmt = default_layer - .with_ansi(true) - .with_level(true) - .boxed(); - let default_subscriber = tracing_subscriber::Registry::default().with(default_fmt).with(default_filter).with(default_indicatif_layer); - let default_guard = tracing::subscriber::set_default(default_subscriber); + let default_guard = dsc_lib_telemetry::basic::BasicTracingOptions::init_default_guard(); // read setting/policy from files if let Ok(v) = get_setting("tracing") { @@ -399,44 +386,18 @@ pub fn enable_tracing(trace_level_arg: Option<&TraceLevel>, trace_format_arg: Op TraceLevel::Debug => Level::DEBUG, TraceLevel::Trace => Level::TRACE, }; - - // enable tracing - let filter = EnvFilter::try_from_default_env() - .or_else(|_| EnvFilter::try_new("warn")) - .unwrap_or_default() - .add_directive(tracing_level.into()); - let indicatif_layer = IndicatifLayer::new(); - let layer = tracing_subscriber::fmt::Layer::default().with_writer(indicatif_layer.get_stderr_writer()); - let with_source = tracing_level == Level::DEBUG || tracing_level == Level::TRACE; - let fmt = match tracing_setting.format { - TraceFormat::Default => { - layer - .with_ansi(true) - .with_level(true) - .with_target(with_source) - .with_line_number(with_source) - .boxed() - }, - TraceFormat::Plaintext => { - layer - .with_ansi(false) - .with_level(true) - .with_target(with_source) - .with_line_number(with_source) - .boxed() - }, - TraceFormat::Json | TraceFormat::PassThrough => { - layer - .with_ansi(false) - .with_level(true) - .with_target(with_source) - .with_line_number(with_source) - .json() - .boxed() - }, + // convert to 'dsc-lib-telemetry' crate type + let stderr_format = match tracing_setting.format { + TraceFormat::Default => StderrFormat::Default, + TraceFormat::Plaintext => StderrFormat::Plaintext, + TraceFormat::Json | TraceFormat::PassThrough => StderrFormat::Json, }; - let subscriber = tracing_subscriber::Registry::default().with(fmt).with(filter).with(indicatif_layer); + // enable tracing + let subscriber = BasicTracingOptions { + tracing_level, + stderr_format + }.init_subscriber(); drop(default_guard); if tracing::subscriber::set_global_default(subscriber).is_err() { diff --git a/lib/dsc-lib-telemetry/.project.data.json b/lib/dsc-lib-telemetry/.project.data.json new file mode 100644 index 000000000..f3c43b68d --- /dev/null +++ b/lib/dsc-lib-telemetry/.project.data.json @@ -0,0 +1,5 @@ +{ + "Name": "dsc-lib-telemetry", + "Kind": "Library", + "IsRust": true +} diff --git a/lib/dsc-lib-telemetry/Cargo.toml b/lib/dsc-lib-telemetry/Cargo.toml new file mode 100644 index 000000000..9d031a31d --- /dev/null +++ b/lib/dsc-lib-telemetry/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dsc-lib-telemetry" +version = "0.0.0" +edition = "2024" + +[lib] +doctest = false # Disable doc tests by default for compilation speed + +[dependencies] +thiserror = { workspace = true } +tracing = { workspace = true } +tracing-indicatif = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/lib/dsc-lib-telemetry/src/basic.rs b/lib/dsc-lib-telemetry/src/basic.rs new file mode 100644 index 000000000..c3a8d0b82 --- /dev/null +++ b/lib/dsc-lib-telemetry/src/basic.rs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! This module provides basic tracing functionality for applications that do not require full +//! OpenTelemetry support. +//! +//! It bundles the [`tracing`] crate and related crates to provide a simple way to emit trace +//! events to stderr in a human-readable format. +//! +//! This module simplifies the process of setting up a tracing subscriber with configurable options +//! for tracing level and stderr output format. It also provides a default guard for capturing +//! events emitted during application initialization, enabling developers to set up their own +//! subscriber later in the application lifecycle. + +// Re-export the tracing crates so that other crates can use them without +// having to add them as separate dependencies. +pub use tracing; +pub use tracing_indicatif; +pub use tracing_subscriber; + +use crate::StderrFormat; + +use tracing::{Level, subscriber::DefaultGuard}; +use tracing_indicatif::IndicatifLayer; +use tracing_subscriber::{ + EnvFilter, + Layer, + Registry, + fmt::Layer as FormatLayer, + layer::{Layered, SubscriberExt} +}; + +// Define type aliases for the boxed layer and the basic tracing subscriber to simplify their usage +// in the code. We don't provide documentation for these type aliases because that overrides the +// documentation for the underlying types, which is more useful to users of this crate. +pub type BoxedLayer = Box + Send + Sync>; +pub type BasicTracingSubscriber = Layered< + IndicatifLayer>>, + Layered> +>; + +/// Represents the options for basic tracing functionality in DSC. +/// +/// This struct allows users to configure the tracing level and the format of stderr output for +/// trace events. It's only used for the legacy basic tracing functionality, not for OpenTelemetry +/// tracing, logging, or metrics. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BasicTracingOptions { + /// Indicates the minimum level of tracing events to be recorded. Events below this level are + /// filtered out and not emitted. The default level is [`Level::INFO`]. + pub tracing_level: Level, + /// Specifies the format of stderr output for trace events. The default format is + /// [`StderrFormat::Default`]. + pub stderr_format: StderrFormat, +} + +impl Default for BasicTracingOptions { + fn default() -> Self { + BasicTracingOptions { + tracing_level: Level::INFO, + stderr_format: StderrFormat::Default, + } + } +} + +impl BasicTracingOptions { + /// Returns a [`DefaultGuard`] that sets up a default tracing subscriber for capturing events + /// emitted during the initialization of the application. + /// + /// The default subscriber is configured with a filter that captures [`Level::WARN`] events and + /// higher, and it uses an [`IndicatifLayer`] for progress reporting. The subscriber is set as + /// the default, allowing it to capture events emitted during initialization. + /// + /// # Example + /// + /// The following example demonstrates how to use this method to setup a default guard that you + /// use during the initialization of your application before dropping it to use your own + /// subscriber. + /// + /// ```rust + /// # use dsc_lib_telemetry::basic::BasicTracingOptions; + /// let default_guard = BasicTracingOptions::init_default_guard(); + /// + /// // Perform initialization tasks that may emit tracing events here; + /// // For this example, we're just assigning values to the tracing level + /// // and stderr format. + /// let tracing_level = tracing::Level::INFO; + /// let stderr_format = dsc_lib_telemetry::StderrFormat::Default; + /// + /// // Initialize your own subscriber based on the desired tracing level + /// // and stderr format. + /// let subscriber = dsc_lib_telemetry::basic::BasicTracingOptions { + /// tracing_level, + /// stderr_format + /// }.init_subscriber(); + /// + /// // Drop the default guard to stop capturing events with the default + /// // subscriber and set your own subscriber as the global default. + /// drop(default_guard); + /// if tracing::subscriber::set_global_default(subscriber).is_err() { + /// eprintln!("Failed to set tracing subscriber as global default."); + /// } + /// ``` + pub fn init_default_guard() -> DefaultGuard { + let default_filter = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("warn")) + .unwrap_or_default() + .add_directive(Level::WARN.into()); + let default_indicatif_layer = IndicatifLayer::new(); + let default_layer = FormatLayer::default() + .with_writer(default_indicatif_layer.get_stderr_writer()); + let default_fmt = default_layer + .with_ansi(true) + .with_level(true) + .boxed(); + let default_subscriber = tracing_subscriber::Registry::default() + .with(default_fmt) + .with(default_filter) + .with(default_indicatif_layer); + + tracing::subscriber::set_default(default_subscriber) + } + + /// Initializes a tracing subscriber based on the provided [`BasicTracingOptions`]. + /// + /// This method sets up a tracing subscriber that captures events at the specified tracing level and formats stderr + /// output according to the specified format. The subscriber is configured with an `IndicatifLayer` for progress + /// reporting. + /// + /// # Example + /// + /// The following example demonstrates how to use this method to initialize a tracing + /// subscriber and set it as the global default subscriber. + /// + /// ```rust + /// # use dsc_lib_telemetry::{basic::BasicTracingOptions, StderrFormat}; + /// # use tracing::Level; + /// + /// let tracing_options = BasicTracingOptions { + /// tracing_level: Level::INFO, + /// stderr_format: StderrFormat::Json, + /// }; + /// let subscriber = tracing_options.init_subscriber(); + /// + /// if tracing::subscriber::set_global_default(subscriber).is_err() { + /// eprintln!("Failed to set tracing subscriber as global default."); + /// } + /// ``` + /// + /// # Returns + /// + /// [`BasicTracingSubscriber`] that can be set as the global default subscriber using + /// [`tracing::subscriber::set_global_default`]. + pub fn init_subscriber(&self) -> BasicTracingSubscriber { + let with_source = self.tracing_level == Level::DEBUG || self.tracing_level == Level::TRACE; + let filter = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("warn")) + .unwrap_or_default() + .add_directive(self.tracing_level.into()); + let indicatif_layer = IndicatifLayer::new(); + let layer = FormatLayer::default() + .with_writer(indicatif_layer.get_stderr_writer()) + .with_level(true) + .with_target(with_source) + .with_line_number(with_source); + let format_layer = match self.stderr_format { + StderrFormat::Default => layer.with_ansi(true).boxed(), + StderrFormat::Plaintext => layer.with_ansi(false).boxed(), + StderrFormat::Json => layer.with_ansi(false).json().boxed(), + }; + + Registry::default() + .with(format_layer) + .with(filter) + .with(indicatif_layer) + } +} diff --git a/lib/dsc-lib-telemetry/src/lib.rs b/lib/dsc-lib-telemetry/src/lib.rs new file mode 100644 index 000000000..128741e18 --- /dev/null +++ b/lib/dsc-lib-telemetry/src/lib.rs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! This library provides telemetry support for DSC. +//! +//! It includes functionality for OpenTelemetry tracing, metrics, and exporting telemetry data to +//! various backends. Other crates can take a dependency on this crate to leverage its OTel +//! capabilities and keep a consistent implementation and dependencies. +//! +//! This crate also supports basic tracing functionality for applications that do not require full +//! OpenTelemetry support. This functionality bundles the [`tracing`] crate and related crates to +//! provide a simple way to emit trace events to stderr in a human-readable format. + +pub mod basic; + +/// Represents the different formats that can be used for stderr output in tracing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StderrFormat { + /// Emit trace events as human-readable text to stderr with ANSI color codes. + Default, + /// Emit trace events as human-readable text to stderr without ANSI color codes. + Plaintext, + /// Emit trace events as JSON to stderr. + Json, +}