diff --git a/README.md b/README.md index 15e8414..1d7420d 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,38 @@ -# Serilog.Sinks.ApplicationInsights [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.ApplicationInsights.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.ApplicationInsights/) +# Serilog.Sinks.ApplicationInsights [![Build status](https://github.com/serilog-contrib/serilog-sinks-applicationinsights/actions/workflows/ci.yml/badge.svg?branch=dev)](https://github.com/serilog-contrib/serilog-sinks-applicationinsights/actions) [![NuGet Version](https://img.shields.io/nuget/v/Serilog.Sinks.ApplicationInsights.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.ApplicationInsights/) [![NuGet Downloads](https://img.shields.io/nuget/dt/Serilog.Sinks.ApplicationInsights.svg)](https://www.nuget.org/packages/Serilog.Sinks.ApplicationInsights/) A sink for Serilog that writes events to Microsoft Application Insights. This sink comes with several defaults that send Serilog `LogEvent` messages to Application Insights as either `EventTelemetry` or `TraceTelemetry`. +## Install + +```powershell +dotnet add package Serilog.Sinks.ApplicationInsights +``` + ## Configuring -The simplest way to configure Serilog to send data to a Application Insights dashboard via instrumentation key is to use -current active *telemetry configuration* which is already initialised in most application types like ASP.NET Core, Azure -Functions etc.: +The recommended way to configure the sink is to reuse the `TelemetryConfiguration` (or `TelemetryClient`) already configured by your application (for example via dependency injection in ASP.NET Core, Azure Functions, Worker Services). ```csharp -var log = new LoggerConfiguration() - .WriteTo.ApplicationInsights(TelemetryConfiguration.Active, TelemetryConverter.Traces) +Log.Logger = new LoggerConfiguration() + .WriteTo.ApplicationInsights( + telemetryConfiguration, // from DI (recommended) + TelemetryConverter.Traces) .CreateLogger(); ``` -.. or as `EventTelemetry`: +If you don't have an existing `TelemetryConfiguration` (uncommon), you can use the connection string overload: ```csharp -var log = new LoggerConfiguration() - .WriteTo.ApplicationInsights(TelemetryConfiguration.Active, TelemetryConverter.Events) +Log.Logger = new LoggerConfiguration() + .WriteTo.ApplicationInsights( + "", + TelemetryConverter.Traces) .CreateLogger(); ``` +Legacy: some older application types used `TelemetryConfiguration.Active`. This is not recommended on modern .NET and may be deprecated depending on the Application Insights SDK version. + > You can also pass an *instrumentation key* and this sink will create a new `TelemetryConfiguration` based on it, > however it's actively discouraged compared to using already initialised telemetry configuration, as your telemetry > won't @@ -48,7 +58,7 @@ in `ConfigureServices`. Log.Logger = new LoggerConfiguration() .WriteTo.ApplicationInsights( serviceProvider.GetRequiredService(), - TelemetryConverter.Traces) + TelemetryConverter.Traces) .CreateLogger(); ``` @@ -57,10 +67,10 @@ startup errors can be caught and properly logged. The problem is that now we're to setup the logger early, but we need the `TelemetryConfiguration` which still haven't been added to our DI container. Luckily [from version 4.0.x of the `Serilog.Extensions.Hosting` we have the possibility to configure a bootstrap logger](https://nblumhardt.com/2020/10/bootstrap-logger/) -to capture early errors, and then change it using DI dependant services once they are configured. +to capture early errors, and then change it using DI-dependent services once they are configured. ```csharp -// dotnet add package serilog.extensions.hosting -v 4.0.0-* +// dotnet add package Serilog.Extensions.Hosting public static class Program { @@ -88,8 +98,8 @@ public static class Program Host.CreateDefaultBuilder(args) .UseSerilog((context, services, loggerConfiguration) => loggerConfiguration .WriteTo.ApplicationInsights( - services.GetRequiredService(), - TelemetryConverter.Traces)) + services.GetRequiredService(), + TelemetryConverter.Traces)) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); } ``` @@ -120,7 +130,7 @@ with [ReadFrom.Configuration(configuration)](https://github.com/serilog/serilog- "Args": { "connectionString": "[your connection string here]", "telemetryConverter": - "Serilog.Sinks.ApplicationInsights.TelemetryConverters.TraceTelemetryConverter, Serilog.Sinks.ApplicationInsights" + "Serilog.Sinks.ApplicationInsights.TelemetryConverters.TraceTelemetryConverter, Serilog.Sinks.ApplicationInsights" } } ], @@ -143,6 +153,10 @@ By default, trace telemetry submits: - **rendered message** in trace's standard *message* property. - **severity** in trace's standard *severityLevel* property. - **timestamp** in trace's standard *timestamp* property. +- **operation id** from `operationId` property, or the `LogEvent.TraceId` property. +- **operation parent id** from `ParentSpanId` property. +- **operation name** from `OperationName` property. +- **component version** from `version` property. - **messageTemplate** in *customDimensions*. - **custom log properties** as *customDimensions*. @@ -151,6 +165,10 @@ Event telemetry submits: - **message template** as *event name*. - **renderedMessage** in *customDimensions*. - **timestamp** in event's standard *timestamp* property. +- **operation id** from `operationId` property, or the `LogEvent.TraceId` property. +- **operation parent id** from `ParentSpanId` property. +- **operation name** from `OperationName` property. +- **component version** from `version` property. - **custom log properties** as *customDimensions*. Exception telemetry submits: @@ -158,6 +176,10 @@ Exception telemetry submits: - **exception** as standard AI exception. - **severity** in trace's standard *severityLevel* property. - **timestamp** in trace's standard *timestamp* property. +- **operation id** from `operationId` property, or the `LogEvent.TraceId` property. +- **operation parent id** from `ParentSpanId` property. +- **operation name** from `OperationName` property. +- **component version** from `version` property. - **custom log properties** as *customDimensions*. > Note that **log context** properties are also included in *customDimensions* when Serilog is configured @@ -232,26 +254,26 @@ private class CustomConverter : TraceTelemetryConverter telemetry.Context.User.Id = logEvent.Properties["UserId"].ToString(); } // post-process the telemetry's context to contain the operation id - if (logEvent.Properties.ContainsKey("operation_Id")) + if (logEvent.Properties.ContainsKey("operationId")) { - telemetry.Context.Operation.Id = logEvent.Properties["operation_Id"].ToString(); + telemetry.Context.Operation.Id = logEvent.Properties["operationId"].ToString(); } // post-process the telemetry's context to contain the operation parent id - if (logEvent.Properties.ContainsKey("operation_parentId")) + if (logEvent.Properties.ContainsKey("ParentSpanId")) { - telemetry.Context.Operation.ParentId = logEvent.Properties["operation_parentId"].ToString(); + telemetry.Context.Operation.ParentId = logEvent.Properties["ParentSpanId"].ToString(); } // typecast to ISupportProperties so you can manipulate the properties as desired - ISupportProperties propTelematry = (ISupportProperties)telemetry; + ISupportProperties propTelemetry = (ISupportProperties)telemetry; // find redundant properties - var removeProps = new[] { "UserId", "operation_parentId", "operation_Id" }; - removeProps = removeProps.Where(prop => propTelematry.Properties.ContainsKey(prop)).ToArray(); + var removeProps = new[] { "UserId", "ParentSpanId", "operationId" }; + removeProps = removeProps.Where(prop => propTelemetry.Properties.ContainsKey(prop)).ToArray(); foreach (var prop in removeProps) { // remove redundant properties - propTelematry.Properties.Remove(prop); + propTelemetry.Properties.Remove(prop); } yield return telemetry; @@ -270,7 +292,7 @@ instance, let's include `renderedMessage` in event telemetry: ```csharp private class IncludeRenderedMessageConverter : EventTelemetryConverter { - public override void ForwardPropertiesToTelemetryProperties(LogEvent logEvent, + public override void ForwardPropertiesToTelemetryProperties(LogEvent logEvent, ISupportProperties telemetryProperties, IFormatProvider formatProvider) { base.ForwardPropertiesToTelemetryProperties(logEvent, telemetryProperties, formatProvider, @@ -299,18 +321,17 @@ You can control when AI shall flush its messages, for example when your applicat // private TelemetryClient _telemetryClient; // ... -_telemetryClient = new TelemetryClient() - { - InstrumentationKey = "" - }; +var telemetryConfiguration = TelemetryConfiguration.CreateDefault(); +telemetryConfiguration.ConnectionString = ""; + +_telemetryClient = new TelemetryClient(telemetryConfiguration); ``` 2) Use that custom `TelemetryClient` to initialize the Sink: ```csharp var log = new LoggerConfiguration() - .WriteTo - .ApplicationInsights(_telemetryClient, TelemetryConverter.Events) + .WriteTo.ApplicationInsights(_telemetryClient, TelemetryConverter.Events) .CreateLogger(); ``` @@ -325,7 +346,7 @@ _telemetryClient.Flush(); await Task.Delay(1000); -// or +// or System.Threading.Thread.Sleep(1000); @@ -333,8 +354,10 @@ System.Threading.Thread.Sleep(1000); ## Including Operation Id -Application Insight's operation id is pushed out if you set `operationId` LogEvent property. If it's present, AI's -operation id will be overridden by the value from this property. +Application Insight's operation id is set from the following sources in order of precedence: + +1. `operationId` LogEvent property +2. `TraceId` LogEvent property This can be set like so: @@ -357,11 +380,70 @@ public class OperationIdEnricher : ILogEventEnricher Application Insight supports component version and is pushed out if you set `version` log event property. If it's present, AI's operation version will include the value from this property. +## Using with SerilogTracing + +[SerilogTracing](https://github.com/serilog-tracing/serilog-tracing) provides tracing primitives that integrate with Serilog's structured logging. When used with this sink, tracing context is automatically included in Application Insights telemetry. + +The following `LogEvent` properties are mapped to Application Insights telemetry: + +| LogEvent Property | Application Insights Telemetry | Notes | +|-------------------|---------------------------------|-------| +| `TraceId` | `Context.Operation.Id` | From TraceId captured in LogEvent | +| `SpanId` | `Id` (for Request/Dependency telemetry) | From SpanId captured in LogEvent | +| `ParentSpanId` | `Context.Operation.ParentId` | | +| `OperationName` | `Context.Operation.Name` | | +| `operationId` | `Context.Operation.Id` | Overrides TraceId | +| `version` | `Context.Component.Version` | | + +If present, `Baggage` is forwarded to Application Insights custom dimensions (`telemetry.Properties`). + +Precedence for `Context.Operation.Id`: `operationId` property > `TraceId` property (when both `operationId` and `TraceId` properties are absent). + +### Enriching from `Activity` (explicit opt-in) + +This sink is designed to work well with Serilog's asynchronous/batched processing. To keep telemetry deterministic, adding `OperationName` and `Baggage` from the ambient `Activity` is an explicit opt-in: copy the values onto the `LogEvent` before it reaches the sink. + +Two built-in enrichers are included: + +- `ActivityOperationNameEnricher` — copies `Activity.OperationName` into the `OperationName` log event property. +- `ActivityBaggageEnricher` — copies baggage items from the current `Activity` into the `Baggage` log event property as a `StructureValue`. + +Enable them using the provided `Enrich` extension methods: + +```csharp +Log.Logger = new LoggerConfiguration() + .Enrich.WithOperationName() + .Enrich.WithBaggage() + .WriteTo.ApplicationInsights(telemetryConfiguration, TelemetryConverter.Traces) + .CreateLogger(); +``` + +## Upgrading to 5.0 (from 4.x) + +This is a new major release (5.0). Notable changes: + +- **OperationName and Baggage are opt-in:** they are only forwarded when present as `LogEvent` properties (use the built-in enrichers above or your own enricher). +- **Less redundancy in custom dimensions by default:** operation-related values are set on `ITelemetry.Context` and are not duplicated into `telemetry.Properties` unless enabled. + +### `TelemetryConverterBase` constructor flags + +Converters derived from `TelemetryConverterBase` can be configured to also include selected operation-related values in `telemetry.Properties` (custom dimensions): + +```csharp +public TelemetryConverterBase( + bool includeOperationIdPropertyAsTelemetryProperty, + bool includeParentSpanIdPropertyAsTelemetryProperty, + bool includeOperationNamePropertyAsTelemetryProperty, + bool includeVersionPropertyAsTelemetryProperty) +``` + +If you previously relied on these values being present in `telemetry.Properties`, enable the relevant flags when constructing your converter, or post-process telemetry in a custom converter. + ## Using with Azure Functions Azure functions has out of the box integration with Application Insights, which automatically logs functions execution start, end, and any exception. Please refer to -the [original documenation](https://docs.microsoft.com/en-us/azure/azure-functions/functions-monitoring) on how to +the [original documentation](https://docs.microsoft.com/en-us/azure/azure-functions/functions-monitoring) on how to enable it. This sink can enrich AI messages, preserving *operation_Id* and other context information which is *already provided by @@ -377,7 +459,7 @@ namespace MyFunctions { public override void Configure(IFunctionsHostBuilder builder) { - builder.Services.AddSingleton((sp) => + builder.Services.AddSingleton((sp) => { Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() @@ -390,7 +472,7 @@ namespace MyFunctions } ``` -Copyright © 2022 Serilog Contributors - Provided under +Copyright © 2025 Serilog Contributors - Provided under the [Apache License, Version 2.0](http://apache.org/licenses/LICENSE-2.0.html). See also: [Serilog Documentation](https://github.com/serilog/serilog/wiki) diff --git a/src/Serilog.Sinks.ApplicationInsights/LoggerConfigurationApplicationInsightsExtensions.cs b/src/Serilog.Sinks.ApplicationInsights/LoggerConfigurationApplicationInsightsExtensions.cs index 0c6745c..5643658 100644 --- a/src/Serilog.Sinks.ApplicationInsights/LoggerConfigurationApplicationInsightsExtensions.cs +++ b/src/Serilog.Sinks.ApplicationInsights/LoggerConfigurationApplicationInsightsExtensions.cs @@ -1,11 +1,11 @@ // Copyright 2016 Serilog Contributors -// +// // 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. @@ -49,8 +49,7 @@ public static LoggerConfiguration ApplicationInsights( var client = new TelemetryClient(telemetryConfiguration ?? TelemetryConfiguration.Active); #pragma warning restore CS0618 - return loggerConfiguration.Sink(new ApplicationInsightsSink(client, telemetryConverter), - restrictedToMinimumLevel, levelSwitch); + return loggerConfiguration.ApplicationInsights(client, telemetryConverter, restrictedToMinimumLevel, levelSwitch); } /// @@ -72,8 +71,7 @@ public static LoggerConfiguration ApplicationInsights( var client = new TelemetryClient(TelemetryConfiguration.Active); #pragma warning restore CS0618 - return loggerConfiguration.Sink(new ApplicationInsightsSink(client, telemetryConverter), - restrictedToMinimumLevel, levelSwitch); + return loggerConfiguration.ApplicationInsights(client, telemetryConverter, restrictedToMinimumLevel, levelSwitch); } /// @@ -93,11 +91,9 @@ public static LoggerConfiguration ApplicationInsights( LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, LoggingLevelSwitch levelSwitch = null) { - return loggerConfiguration.Sink(new ApplicationInsightsSink(telemetryClient, telemetryConverter), - restrictedToMinimumLevel, levelSwitch); + return loggerConfiguration.Sink(new ApplicationInsightsSink(telemetryClient, telemetryConverter), restrictedToMinimumLevel, levelSwitch); } - /// /// Adds a Serilog sink that writes log events to Microsoft Application Insights /// using a custom converter / constructor. Only use in rare cases when your application @@ -125,7 +121,6 @@ public static LoggerConfiguration ApplicationInsights( var client = new TelemetryClient(config); - return loggerConfiguration.Sink(new ApplicationInsightsSink(client, telemetryConverter), - restrictedToMinimumLevel, levelSwitch); + return loggerConfiguration.ApplicationInsights(client, telemetryConverter, restrictedToMinimumLevel, levelSwitch); } -} \ No newline at end of file +} diff --git a/src/Serilog.Sinks.ApplicationInsights/LoggerEnrichmentConfigurationExtensions.cs b/src/Serilog.Sinks.ApplicationInsights/LoggerEnrichmentConfigurationExtensions.cs new file mode 100644 index 0000000..231cf22 --- /dev/null +++ b/src/Serilog.Sinks.ApplicationInsights/LoggerEnrichmentConfigurationExtensions.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2025 Serilog Contributors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Serilog.Configuration; +using Serilog.Sinks.ApplicationInsights.Enrichers; + +namespace Serilog; + +/// +/// Provides enrichment extensions to . +/// +public static class LoggerEnrichmentConfigurationExtensions +{ + extension(LoggerEnrichmentConfiguration loggerEnrichmentConfiguration) + { + /// + /// Enriches log events with the operation name from . + /// + /// Logger configuration, allowing configuration to continue. + public LoggerConfiguration WithOperationName() + => loggerEnrichmentConfiguration.With(); + + /// + /// Enriches log events with the baggage from . + /// + /// Logger configuration, allowing configuration to continue. + public LoggerConfiguration WithBaggage() + => loggerEnrichmentConfiguration.With(); + } +} diff --git a/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/ApplicationInsightsSink.cs b/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/ApplicationInsightsSink.cs index f87c851..3eda2f3 100644 --- a/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/ApplicationInsightsSink.cs +++ b/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/ApplicationInsightsSink.cs @@ -1,22 +1,19 @@ // Copyright 2016 Serilog Contributors -// +// // 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. -using System; using System.Reflection; using System.Runtime.ExceptionServices; -using System.Threading; -using System.Threading.Tasks; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Channel; using Serilog.Core; @@ -121,7 +118,7 @@ public virtual void Emit(LogEvent logEvent) #endregion - #region AI specifc Helper methods + #region AI specific Helper methods /// /// Hands over the to the AI telemetry client. @@ -139,7 +136,7 @@ protected virtual void TrackTelemetry(ITelemetry telemetry) _telemetryClient?.Track(telemetry); } - #endregion AI specifc Helper methods + #endregion AI specific Helper methods #region Implementation of IDisposable @@ -195,7 +192,7 @@ protected virtual void Dispose(bool disposeManagedResources) IsDisposing = false; } } - + #if NET6_0_OR_GREATER /// /// Disposes the sink and flushes telemetry to App Insights. @@ -204,7 +201,7 @@ public async ValueTask DisposeAsync() { if (IsDisposing || IsDisposed) return; - + try { IsDisposing = true; @@ -220,4 +217,4 @@ public async ValueTask DisposeAsync() #endif #endregion Implementation of IDisposable -} \ No newline at end of file +} diff --git a/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/Enrichers/ActivityBaggageEnricher.cs b/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/Enrichers/ActivityBaggageEnricher.cs new file mode 100644 index 0000000..f2051f8 --- /dev/null +++ b/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/Enrichers/ActivityBaggageEnricher.cs @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2025 Serilog Contributors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Serilog.Core; +using Serilog.Events; +using Serilog.Sinks.ApplicationInsights.TelemetryConverters; + +namespace Serilog.Sinks.ApplicationInsights.Enrichers; + +/// +/// Enriches log events with the baggage from . +/// +public class ActivityBaggageEnricher : ILogEventEnricher +{ + /// + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (logEvent == null) + { + throw new ArgumentNullException(nameof(logEvent)); + } + + if (propertyFactory == null) + { + throw new ArgumentNullException(nameof(propertyFactory)); + } + + Activity activity = Activity.Current; + if (activity is null) + { + return; + } + + IEnumerable items = activity.Baggage + .Where(IsValidBaggageItem) + .Select(item => propertyFactory.CreateProperty(item.Key, item.Value)); + + LogEventProperty baggageProperty = new(TelemetryConverterBase.BaggageProperty, new StructureValue(items)); + logEvent.AddPropertyIfAbsent(baggageProperty); + } + + private static bool IsValidBaggageItem(KeyValuePair item) + => !string.IsNullOrEmpty(item.Key) && !string.IsNullOrEmpty(item.Value); +} diff --git a/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/Enrichers/ActivityOperationNameEnricher.cs b/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/Enrichers/ActivityOperationNameEnricher.cs new file mode 100644 index 0000000..516b447 --- /dev/null +++ b/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/Enrichers/ActivityOperationNameEnricher.cs @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2025 Serilog Contributors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Serilog.Core; +using Serilog.Events; +using Serilog.Sinks.ApplicationInsights.TelemetryConverters; + +namespace Serilog.Sinks.ApplicationInsights.Enrichers; + +/// +/// Enriches log events with the current operation name from . +/// +public class ActivityOperationNameEnricher : ILogEventEnricher +{ + /// + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (logEvent == null) + { + throw new ArgumentNullException(nameof(logEvent)); + } + + if (propertyFactory == null) + { + throw new ArgumentNullException(nameof(propertyFactory)); + } + + string operationName = Activity.Current?.OperationName; + if (operationName is null) + { + return; + } + + LogEventProperty operationNameProperty = propertyFactory.CreateProperty(TelemetryConverterBase.OperationNameProperty, operationName); + logEvent.AddPropertyIfAbsent(operationNameProperty); + } +} diff --git a/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/TelemetryConverters/EventTelemetryConverter.cs b/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/TelemetryConverters/EventTelemetryConverter.cs index e1cbb3e..4382489 100644 --- a/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/TelemetryConverters/EventTelemetryConverter.cs +++ b/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/TelemetryConverters/EventTelemetryConverter.cs @@ -10,13 +10,34 @@ namespace Serilog.Sinks.ApplicationInsights.TelemetryConverters; public class EventTelemetryConverter : TelemetryConverterBase { + /// + public EventTelemetryConverter() + : this(false, false, false, false) + { + } + + /// + public EventTelemetryConverter( + bool includeOperationIdPropertyAsTelemetryProperty, + bool includeParentSpanIdPropertyAsTelemetryProperty, + bool includeOperationNamePropertyAsTelemetryProperty, + bool includeVersionPropertyAsTelemetryProperty) + : base( + includeOperationIdPropertyAsTelemetryProperty, + includeParentSpanIdPropertyAsTelemetryProperty, + includeOperationNamePropertyAsTelemetryProperty, + includeVersionPropertyAsTelemetryProperty) + { + } + public override IEnumerable Convert(LogEvent logEvent, IFormatProvider formatProvider) { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); if (logEvent.Exception == null) { - var telemetry = new EventTelemetry(logEvent.MessageTemplate.Text) { + var telemetry = new EventTelemetry(logEvent.MessageTemplate.Text) + { Timestamp = logEvent.Timestamp }; @@ -39,4 +60,4 @@ public override void ForwardPropertiesToTelemetryProperties(LogEvent logEvent, includeRenderedMessage: true, includeMessageTemplate: false); } -} \ No newline at end of file +} diff --git a/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/TelemetryConverters/TelemetryConverterBase.cs b/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/TelemetryConverters/TelemetryConverterBase.cs index ceab540..d0061a2 100644 --- a/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/TelemetryConverters/TelemetryConverterBase.cs +++ b/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/TelemetryConverters/TelemetryConverterBase.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; +using System.Diagnostics; using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; using Serilog.Events; @@ -39,6 +35,21 @@ public abstract class TelemetryConverterBase : ITelemetryConverter /// public const string OperationIdProperty = "operationId"; + /// + /// Property that is included when in log context, will be pushed out as AI parent span id. + /// + public const string ParentSpanIdProperty = "ParentSpanId"; + + /// + /// Property that is included when in log context, will be pushed out as AI operation name. + /// + public const string OperationNameProperty = "OperationName"; + + /// + /// Property that is included when in log context, will be pushed out as AI telemetry properties. + /// + public const string BaggageProperty = "Baggage"; + /// /// Property that is included when in log context, will be pushed out as AI component version. /// @@ -46,13 +57,52 @@ public abstract class TelemetryConverterBase : ITelemetryConverter static readonly MessageTemplateTextFormatter MessageTemplateTextFormatter = new("{Message:lj}"); + private readonly bool _includeOperationIdPropertyAsTelemetryProperty; + private readonly bool _includeParentSpanIdPropertyAsTelemetryProperty; + private readonly bool _includeOperationNamePropertyAsTelemetryProperty; + private readonly bool _includeVersionPropertyAsTelemetryProperty; + /// /// Creates an instance of using default value formatter ( /// ). /// public TelemetryConverterBase() + : this(false, false, false, false) + { + } + + /// + /// Creates an instance of using default value formatter ( + /// ). + /// + /// + /// if set to true the is added to the + /// telemetry properties. Otherwise it is only set as ITelemetry.Context.Operation.Id. + /// + /// + /// if set to true the is added to the + /// telemetry properties. Otherwise it is only set as ITelemetry.Context.Operation.ParentId. + /// + /// + /// if set to true the is added to the + /// telemetry properties. Otherwise it is only set as ITelemetry.Context.Operation.Name. + /// + /// + /// if set to true the is added to the + /// telemetry properties. Otherwise it is only set as ITelemetry.Context.Component.Version. + /// + public TelemetryConverterBase( + bool includeOperationIdPropertyAsTelemetryProperty, + bool includeParentSpanIdPropertyAsTelemetryProperty, + bool includeOperationNamePropertyAsTelemetryProperty, + bool includeVersionPropertyAsTelemetryProperty) { ValueFormatter = new ApplicationInsightsJsonValueFormatter(); + + _includeOperationIdPropertyAsTelemetryProperty = includeOperationIdPropertyAsTelemetryProperty; + _includeParentSpanIdPropertyAsTelemetryProperty = includeParentSpanIdPropertyAsTelemetryProperty; + _includeOperationNamePropertyAsTelemetryProperty = includeOperationNamePropertyAsTelemetryProperty; + _includeVersionPropertyAsTelemetryProperty = includeVersionPropertyAsTelemetryProperty; } #pragma warning disable CS1591 @@ -72,7 +122,8 @@ public virtual ExceptionTelemetry ToExceptionTelemetry( if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); if (logEvent.Exception == null) throw new ArgumentException("Must have an Exception", nameof(logEvent)); - var exceptionTelemetry = new ExceptionTelemetry(logEvent.Exception) { + var exceptionTelemetry = new ExceptionTelemetry(logEvent.Exception) + { SeverityLevel = ToSeverityLevel(logEvent.Level), Timestamp = logEvent.Timestamp }; @@ -143,25 +194,85 @@ public void ForwardPropertiesToTelemetryProperties(LogEvent logEvent, if (telemetryProperties is ITelemetry telemetry) { - if (logEvent.Properties.TryGetValue(OperationIdProperty, out var operationId)) - telemetry.Context.Operation.Id = operationId.ToString().Trim('\"'); - else - { - if (logEvent.TraceId is ActivityTraceId traceId) - telemetry.Context.Operation.Id = traceId.ToHexString(); + PopulateTelemetryFromLogEvent(logEvent, telemetry); - if (logEvent.SpanId is ActivitySpanId spanId) - telemetry.Context.Operation.ParentId = spanId.ToHexString(); + if (telemetry.Context?.Component != null + && logEvent.Properties.TryGetValue(VersionProperty, out var version)) + telemetry.Context.Component.Version = version.ToString().Trim('\"'); + } + + var baggageWasForwarded = ForwardActivityBaggage(logEvent, telemetryProperties, formatProvider); + ForwardSimpleProperties(logEvent, telemetryProperties, baggageWasForwarded); + } + + private static void PopulateTelemetryFromLogEvent(LogEvent logEvent, ITelemetry telemetry) + { + // Operation.Id (TraceId) + if (logEvent.Properties.TryGetValue(OperationIdProperty, out var operationIdProp)) + telemetry.Context.Operation.Id = operationIdProp.ToString().Trim('"'); + else if (logEvent.TraceId is ActivityTraceId traceId) + telemetry.Context.Operation.Id = traceId.ToHexString(); + + // Operation.ParentId (ParentSpanId) + if (logEvent.Properties.TryGetValue(ParentSpanIdProperty, out var parentSpanIdProp)) + telemetry.Context.Operation.ParentId = parentSpanIdProp.ToString().Trim('"'); + + // Operation.Name (OperationName) + if (logEvent.Properties.TryGetValue(OperationNameProperty, out var operationNameProp)) + telemetry.Context.Operation.Name = operationNameProp.ToString().Trim('"'); + + // Set Id for RequestTelemetry and DependencyTelemetry + if (logEvent.SpanId is ActivitySpanId spanId) + { + if (telemetry is RequestTelemetry req) + req.Id = spanId.ToHexString(); + else if (telemetry is DependencyTelemetry dep) + dep.Id = spanId.ToHexString(); + } + } + + private static bool ForwardActivityBaggage(LogEvent logEvent, ISupportProperties telemetryProperties, IFormatProvider formatProvider) + { + if (!logEvent.Properties.TryGetValue(BaggageProperty, out var baggageProp) + || baggageProp is not StructureValue baggageStructure) + { + return false; + } + + foreach (var item in baggageStructure.Properties) + { + var key = item.Name; + if (telemetryProperties.Properties.ContainsKey(key)) + { + continue; } - if (logEvent.Properties.TryGetValue(VersionProperty, out var version) - && telemetry.Context?.Component != null) - telemetry.Context.Component.Version = version.ToString().Trim('\"'); + var value = item.Value.ToString(null, formatProvider).Trim('"'); + telemetryProperties.Properties.Add(key, value); } - foreach (var property in logEvent.Properties.Where(property => - property.Value != null && !telemetryProperties.Properties.ContainsKey(property.Key))) + return true; + } + + private void ForwardSimpleProperties(LogEvent logEvent, ISupportProperties telemetryProperties, bool skipBaggage) + { + var skipOperationId = !_includeOperationIdPropertyAsTelemetryProperty; + var skipParentSpanId = !_includeParentSpanIdPropertyAsTelemetryProperty; + var skipOperationName = !_includeOperationNamePropertyAsTelemetryProperty; + var skipVersion = !_includeVersionPropertyAsTelemetryProperty; + + foreach (var property in logEvent.Properties) + { + if (property.Value is null) continue; + if (skipOperationId && OperationIdProperty.Equals(property.Key, StringComparison.Ordinal)) continue; + if (skipParentSpanId && ParentSpanIdProperty.Equals(property.Key, StringComparison.Ordinal)) continue; + if (skipOperationName && OperationNameProperty.Equals(property.Key, StringComparison.Ordinal)) continue; + if (skipVersion && VersionProperty.Equals(property.Key, StringComparison.Ordinal)) continue; + if (skipBaggage && BaggageProperty.Equals(property.Key, StringComparison.Ordinal)) continue; + if (telemetryProperties.Properties.ContainsKey(property.Key)) continue; + ValueFormatter.Format(property.Key, property.Value, telemetryProperties.Properties); + } } /// @@ -188,4 +299,4 @@ public void ForwardPropertiesToTelemetryProperties(LogEvent logEvent, return null; } -} \ No newline at end of file +} diff --git a/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/TelemetryConverters/TraceTelemetryConverter.cs b/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/TelemetryConverters/TraceTelemetryConverter.cs index 61277fd..93880ae 100644 --- a/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/TelemetryConverters/TraceTelemetryConverter.cs +++ b/src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/TelemetryConverters/TraceTelemetryConverter.cs @@ -14,6 +14,26 @@ public class TraceTelemetryConverter : TelemetryConverterBase { static readonly MessageTemplateTextFormatter MessageTemplateTextFormatter = new("{Message:lj}"); + /// + public TraceTelemetryConverter() + : this(false, false, false, false) + { + } + + /// + public TraceTelemetryConverter( + bool includeOperationIdPropertyAsTelemetryProperty, + bool includeParentSpanIdPropertyAsTelemetryProperty, + bool includeOperationNamePropertyAsTelemetryProperty, + bool includeVersionPropertyAsTelemetryProperty) + : base( + includeOperationIdPropertyAsTelemetryProperty, + includeParentSpanIdPropertyAsTelemetryProperty, + includeOperationNamePropertyAsTelemetryProperty, + includeVersionPropertyAsTelemetryProperty) + { + } + public override IEnumerable Convert(LogEvent logEvent, IFormatProvider formatProvider) { if (logEvent == null) @@ -24,7 +44,8 @@ public override IEnumerable Convert(LogEvent logEvent, IFormatProvid var sw = new StringWriter(); MessageTemplateTextFormatter.Format(logEvent, sw); - var telemetry = new TraceTelemetry(sw.ToString()) { + var telemetry = new TraceTelemetry(sw.ToString()) + { Timestamp = logEvent.Timestamp, SeverityLevel = ToSeverityLevel(logEvent.Level) }; @@ -39,4 +60,4 @@ public override IEnumerable Convert(LogEvent logEvent, IFormatProvid yield return ToExceptionTelemetry(logEvent, formatProvider); } } -} \ No newline at end of file +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/ApplicationInsightsTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/ApplicationInsightsTest.cs index c4a644b..4f668e4 100644 --- a/test/Serilog.Sinks.ApplicationInsights.Tests/ApplicationInsightsTest.cs +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/ApplicationInsightsTest.cs @@ -11,15 +11,26 @@ public abstract class ApplicationInsightsTest { readonly UnitTestTelemetryChannel _channel; - protected ApplicationInsightsTest(ITelemetryConverter converter = null) + protected ApplicationInsightsTest(ITelemetryConverter converter = null, bool addOperationNameEnricher = false, bool addBaggageEnricher = false) { var tc = new TelemetryConfiguration { TelemetryChannel = _channel = new UnitTestTelemetryChannel() }; - Logger = new LoggerConfiguration() + var loggerConfiguration = new LoggerConfiguration() .WriteTo.ApplicationInsights(tc, converter ?? TelemetryConverter.Traces) .MinimumLevel.Debug() - .Enrich.FromLogContext() - .CreateLogger(); + .Enrich.FromLogContext(); + + if (addOperationNameEnricher) + { + loggerConfiguration = loggerConfiguration.Enrich.WithOperationName(); + } + + if (addBaggageEnricher) + { + loggerConfiguration = loggerConfiguration.Enrich.WithBaggage(); + } + + Logger = loggerConfiguration.CreateLogger(); } protected ILogger Logger { get; } @@ -38,4 +49,4 @@ protected ApplicationInsightsTest(ITelemetryConverter converter = null) _channel.SubmittedTelemetry .OfType() .LastOrDefault(); -} \ No newline at end of file +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/Enrichers/ActivityBaggageEnricherTests.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/Enrichers/ActivityBaggageEnricherTests.cs new file mode 100644 index 0000000..c044942 --- /dev/null +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/Enrichers/ActivityBaggageEnricherTests.cs @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2025 Serilog Contributors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Serilog.Events; +using Serilog.Sinks.ApplicationInsights.Enrichers; +using Xunit; + +namespace Serilog.Sinks.ApplicationInsights.Tests.Enrichers; + +public class ActivityBaggageEnricherTests +{ + [Fact] + public void Single_baggage_value_is_enriched() + { + ActivityBaggageEnricher enricher = new(); + using Activity activity = new("TestActivity"); + string baggageName = Guid.NewGuid().ToString("N"); + string baggageValue = Guid.NewGuid().ToString("N"); + activity.AddBaggage(baggageName, baggageValue); + activity.Start(); + LogEvent logEvent = new(DateTimeOffset.Now, LogEventLevel.Information, null, MessageTemplate.Empty, []); + + enricher.Enrich(logEvent, TestLogEventPropertyFactory.Instance); + + bool hasProperty = logEvent.Properties.TryGetValue("Baggage", out LogEventPropertyValue property); + Assert.True(hasProperty); + Assert.NotNull(property); + Assert.IsType(property); + StructureValue scalarValue = (StructureValue)property; + Assert.Single(scalarValue.Properties); + LogEventProperty valueProperty = scalarValue.Properties[0]; + Assert.Equal(baggageName, valueProperty.Name); + Assert.IsType(valueProperty.Value); + ScalarValue scalarValueInner = (ScalarValue)valueProperty.Value; + Assert.Equal(baggageValue, scalarValueInner.Value); + } + + [Fact] + public void Multiple_baggage_values_are_enriched() + { + Random random = new(); + Dictionary baggageItems = GenerateRandomBaggageItems(10); + + ActivityBaggageEnricher enricher = new(); + using Activity activity = new("TestActivity"); + foreach (var item in baggageItems) + { + activity.AddBaggage(item.Key, item.Value); + } + + activity.Start(); + LogEvent logEvent = new(DateTimeOffset.Now, LogEventLevel.Information, null, MessageTemplate.Empty, []); + + enricher.Enrich(logEvent, TestLogEventPropertyFactory.Instance); + + bool hasProperty = logEvent.Properties.TryGetValue("Baggage", out LogEventPropertyValue property); + Assert.True(hasProperty); + Assert.NotNull(property); + Assert.IsType(property); + StructureValue scalarValue = (StructureValue)property; + Assert.Equal(baggageItems.Count, scalarValue.Properties.Count); + + foreach (var item in baggageItems) + { + LogEventProperty valueProperty = scalarValue.Properties.FirstOrDefault(p => p.Name == item.Key); + Assert.NotNull(valueProperty); + Assert.IsType(valueProperty.Value); + ScalarValue scalarValueInner = (ScalarValue)valueProperty.Value; + Assert.Equal(item.Value, scalarValueInner.Value); + } + } + + private static Dictionary GenerateRandomBaggageItems(int count) + { + Dictionary baggageItems = new(count); + for (int i = 0; i < count; i++) + { + string key = Guid.NewGuid().ToString("N"); + string value = Guid.NewGuid().ToString("N"); + baggageItems[key] = value; + } + + return baggageItems; + } +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/Enrichers/ActivityOperationNameEnricherTests.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/Enrichers/ActivityOperationNameEnricherTests.cs new file mode 100644 index 0000000..37b05ed --- /dev/null +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/Enrichers/ActivityOperationNameEnricherTests.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2025 Serilog Contributors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Serilog.Events; +using Serilog.Sinks.ApplicationInsights.Enrichers; +using Xunit; + +namespace Serilog.Sinks.ApplicationInsights.Tests.Enrichers; + +public class ActivityOperationNameEnricherTests +{ + [Fact] + public void Operation_name_is_enriched() + { + ActivityOperationNameEnricher enricher = new(); + string operationName = Guid.NewGuid().ToString("N"); + using Activity activity = new(operationName); + activity.Start(); + LogEvent logEvent = new(DateTimeOffset.Now, LogEventLevel.Information, null, MessageTemplate.Empty, []); + + enricher.Enrich(logEvent, TestLogEventPropertyFactory.Instance); + + bool hasProperty = logEvent.Properties.TryGetValue("OperationName", out LogEventPropertyValue property); + Assert.True(hasProperty); + Assert.NotNull(property); + Assert.IsType(property); + ScalarValue scalarValue = (ScalarValue)property; + Assert.Equal(operationName, scalarValue.Value); + } +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/Enrichers/TestLogEventPropertyFactory.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/Enrichers/TestLogEventPropertyFactory.cs new file mode 100644 index 0000000..6098fae --- /dev/null +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/Enrichers/TestLogEventPropertyFactory.cs @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2025 Serilog Contributors +// SPDX-License-Identifier: Apache-2.0 + +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Sinks.ApplicationInsights.Tests.Enrichers; + +internal class TestLogEventPropertyFactory : ILogEventPropertyFactory +{ + public static TestLogEventPropertyFactory Instance { get; } = new(); + + public LogEventProperty CreateProperty(string name, object value, bool destructureObjects = false) + { + LogEventPropertyValue logEventPropertyValue; + if (destructureObjects && value is IEnumerable logEventProperties) + { + logEventPropertyValue = new StructureValue(logEventProperties); + } + else + { + logEventPropertyValue = new ScalarValue(value); + } + + return new LogEventProperty(name, logEventPropertyValue); + } +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/CustomTelemetryConversionTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/CustomTelemetryConversionTest.cs similarity index 97% rename from test/Serilog.Sinks.ApplicationInsights.Tests/CustomTelemetryConversionTest.cs rename to test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/CustomTelemetryConversionTest.cs index 18cbfb5..2a026cf 100644 --- a/test/Serilog.Sinks.ApplicationInsights.Tests/CustomTelemetryConversionTest.cs +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/CustomTelemetryConversionTest.cs @@ -7,7 +7,7 @@ using Serilog.Sinks.ApplicationInsights.TelemetryConverters; using Xunit; -namespace Serilog.Sinks.ApplicationInsights.Tests; +namespace Serilog.Sinks.ApplicationInsights.Tests.TelemetryConverters; public class CustomTelemetryConversionTest : ApplicationInsightsTest { @@ -55,4 +55,4 @@ public override IEnumerable Convert(LogEvent logEvent, IFormatProvid } } } -} \ No newline at end of file +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/DottedOutFormattingTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/DottedOutFormattingTest.cs similarity index 94% rename from test/Serilog.Sinks.ApplicationInsights.Tests/DottedOutFormattingTest.cs rename to test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/DottedOutFormattingTest.cs index a9494ab..d1838e5 100644 --- a/test/Serilog.Sinks.ApplicationInsights.Tests/DottedOutFormattingTest.cs +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/DottedOutFormattingTest.cs @@ -2,7 +2,7 @@ using Serilog.Sinks.ApplicationInsights.TelemetryConverters; using Xunit; -namespace Serilog.Sinks.ApplicationInsights.Tests; +namespace Serilog.Sinks.ApplicationInsights.Tests.TelemetryConverters; public class DottedOutFormattingTest : ApplicationInsightsTest { @@ -33,4 +33,4 @@ class DottedOutTrace : TraceTelemetryConverter { public override IValueFormatter ValueFormatter => new ApplicationInsightsDottedValueFormatter(); } -} \ No newline at end of file +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/CustomiseEventTelemetryConverterTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/CustomiseEventTelemetryConverterTest.cs similarity index 90% rename from test/Serilog.Sinks.ApplicationInsights.Tests/CustomiseEventTelemetryConverterTest.cs rename to test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/CustomiseEventTelemetryConverterTest.cs index b01cea4..92f48ef 100644 --- a/test/Serilog.Sinks.ApplicationInsights.Tests/CustomiseEventTelemetryConverterTest.cs +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/CustomiseEventTelemetryConverterTest.cs @@ -3,7 +3,7 @@ using Serilog.Events; using Serilog.Sinks.ApplicationInsights.TelemetryConverters; -namespace Serilog.Sinks.ApplicationInsights.Tests; +namespace Serilog.Sinks.ApplicationInsights.Tests.TelemetryConverters.Event; public class CustomiseEventTelemetryConverterTest : ApplicationInsightsTest { @@ -18,4 +18,4 @@ public override void ForwardPropertiesToTelemetryProperties(LogEvent logEvent, includeMessageTemplate: false); } } -} \ No newline at end of file +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/EventTelemetryConverterTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/EventTelemetryConverterTest.cs similarity index 61% rename from test/Serilog.Sinks.ApplicationInsights.Tests/EventTelemetryConverterTest.cs rename to test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/EventTelemetryConverterTest.cs index 32ffb13..42d622f 100644 --- a/test/Serilog.Sinks.ApplicationInsights.Tests/EventTelemetryConverterTest.cs +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/EventTelemetryConverterTest.cs @@ -1,13 +1,12 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using Serilog.Sinks.ApplicationInsights.TelemetryConverters; using Xunit; -namespace Serilog.Sinks.ApplicationInsights.Tests; +namespace Serilog.Sinks.ApplicationInsights.Tests.TelemetryConverters.Event; public class EventTelemetryConverterTest : ApplicationInsightsTest { - public EventTelemetryConverterTest() : base(new EventTelemetryConverter()) + public EventTelemetryConverterTest() : base(new EventTelemetryConverter(), true, true) { } @@ -40,21 +39,19 @@ public void DestructuredPropertyIsFormattedCorrectly() } [Fact] - public void TraceIdAndSpanIdDefaultByDefault() + public void TraceIdIsNullByDefault() { Logger.Information("Hello, {Name}!", "world"); Assert.Null(LastSubmittedEventTelemetry.Context.Operation.Id); - Assert.Null(LastSubmittedEventTelemetry.Context.Operation.ParentId); } [Fact] - public void TraceIdAndSpanIdAreSet() + public void TraceIdIsSet() { using Activity activity = new("TestActivity"); activity.Start(); Logger.Information("Hello, {Name}!", "world"); Assert.Equal(activity.TraceId.ToHexString(), LastSubmittedEventTelemetry.Context.Operation.Id); - Assert.Equal(activity.SpanId.ToHexString(), LastSubmittedEventTelemetry.Context.Operation.ParentId); } [Fact] @@ -67,4 +64,43 @@ public void OperationIdTakesPrecedenceOverTraceId() Assert.Equal(operationId, LastSubmittedEventTelemetry.Context.Operation.Id); Assert.Null(LastSubmittedEventTelemetry.Context.Operation.ParentId); } + + [Fact] + public void ParentSpanIdIsSet() + { + Logger.Information("Test {ParentSpanId}", "parent123"); + Assert.Equal("parent123", LastSubmittedEventTelemetry.Context.Operation.ParentId); + } + + [Fact] + public void VersionIsSet() + { + Logger.Information("Test {version}", "1.2.3"); + Assert.Equal("1.2.3", LastSubmittedEventTelemetry.Context.Component.Version); + } + + [Fact] + public void OperationNameIsSet() + { + using Activity activity = new("MyOperation"); + activity.Start(); + + Logger.Information("Test"); + + Assert.Equal("MyOperation", LastSubmittedEventTelemetry.Context.Operation.Name); + } + + [Fact] + public void BaggageIsSet() + { + using Activity activity = new("TestActivity"); + activity.AddBaggage("key1", "value1"); + activity.AddBaggage("key2", "value2"); + activity.Start(); + + Logger.Information("Hello, world!"); + + Assert.Equal("value1", LastSubmittedEventTelemetry.Properties["key1"]); + Assert.Equal("value2", LastSubmittedEventTelemetry.Properties["key2"]); + } } diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/IncludeOperationIdTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/IncludeOperationIdTest.cs new file mode 100644 index 0000000..730cbfa --- /dev/null +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/IncludeOperationIdTest.cs @@ -0,0 +1,23 @@ +using Serilog.Sinks.ApplicationInsights.TelemetryConverters; +using Xunit; + +namespace Serilog.Sinks.ApplicationInsights.Tests.TelemetryConverters.Event; + +public class IncludeOperationIdTest : ApplicationInsightsTest +{ + public IncludeOperationIdTest() + : base(new EventTelemetryConverter(true, false, false, false), true, true) + { + } + + [Fact] + public void OperationIdIsSetAsTraceProperty() + { + using var activity = new System.Diagnostics.Activity("TestActivity"); + activity.Start(); + + Logger.Information("Hello, {operationId}!", "foo-operation-id"); + + Assert.Equal("foo-operation-id", LastSubmittedEventTelemetry.Properties["operationId"]); + } +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/IncludeOperationNameTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/IncludeOperationNameTest.cs new file mode 100644 index 0000000..ebd51b6 --- /dev/null +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/IncludeOperationNameTest.cs @@ -0,0 +1,23 @@ +using Serilog.Sinks.ApplicationInsights.TelemetryConverters; +using Xunit; + +namespace Serilog.Sinks.ApplicationInsights.Tests.TelemetryConverters.Event; + +public class IncludeOperationNameTest : ApplicationInsightsTest +{ + public IncludeOperationNameTest() + : base(new EventTelemetryConverter(false, false, true, false), true, true) + { + } + + [Fact] + public void OperationIdIsSetAsTraceProperty() + { + using var activity = new System.Diagnostics.Activity("TestActivity"); + activity.Start(); + + Logger.Information("Hello, {OperationName}!", "foo-operation-name"); + + Assert.Equal("foo-operation-name", LastSubmittedEventTelemetry.Properties["OperationName"]); + } +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/IncludeParentSpanIdTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/IncludeParentSpanIdTest.cs new file mode 100644 index 0000000..2e07e2e --- /dev/null +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/IncludeParentSpanIdTest.cs @@ -0,0 +1,23 @@ +using Serilog.Sinks.ApplicationInsights.TelemetryConverters; +using Xunit; + +namespace Serilog.Sinks.ApplicationInsights.Tests.TelemetryConverters.Event; + +public class IncludeSpanIdTest : ApplicationInsightsTest +{ + public IncludeSpanIdTest() + : base(new EventTelemetryConverter(false, true, false, false), true, true) + { + } + + [Fact] + public void OperationIdIsSetAsTraceProperty() + { + using var activity = new System.Diagnostics.Activity("TestActivity"); + activity.Start(); + + Logger.Information("Hello, {ParentSpanId}!", "foo-parent-span-id"); + + Assert.Equal("foo-parent-span-id", LastSubmittedEventTelemetry.Properties["ParentSpanId"]); + } +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/IncludeVersionTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/IncludeVersionTest.cs new file mode 100644 index 0000000..23f89a2 --- /dev/null +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Event/IncludeVersionTest.cs @@ -0,0 +1,23 @@ +using Serilog.Sinks.ApplicationInsights.TelemetryConverters; +using Xunit; + +namespace Serilog.Sinks.ApplicationInsights.Tests.TelemetryConverters.Event; + +public class IncludeVersionTest : ApplicationInsightsTest +{ + public IncludeVersionTest() + : base(new EventTelemetryConverter(false, false, false, true), true, true) + { + } + + [Fact] + public void OperationIdIsSetAsTraceProperty() + { + using var activity = new System.Diagnostics.Activity("TestActivity"); + activity.Start(); + + Logger.Information("Hello, {version}!", "v1.3.3.7"); + + Assert.Equal("v1.3.3.7", LastSubmittedEventTelemetry.Properties["version"]); + } +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConversionTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/TelemetryConversionTest.cs similarity index 94% rename from test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConversionTest.cs rename to test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/TelemetryConversionTest.cs index 0445881..473a617 100644 --- a/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConversionTest.cs +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/TelemetryConversionTest.cs @@ -7,7 +7,7 @@ using Serilog.Sinks.ApplicationInsights.TelemetryConverters; using Xunit; -namespace Serilog.Sinks.ApplicationInsights.Tests; +namespace Serilog.Sinks.ApplicationInsights.Tests.TelemetryConverters; public class TelemetryConversionTest : ApplicationInsightsTest { @@ -48,4 +48,4 @@ public override IEnumerable Convert(LogEvent logEvent, IFormatProvid } } } -} \ No newline at end of file +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/IncludeOperationIdTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/IncludeOperationIdTest.cs new file mode 100644 index 0000000..8c20eb2 --- /dev/null +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/IncludeOperationIdTest.cs @@ -0,0 +1,23 @@ +using Serilog.Sinks.ApplicationInsights.TelemetryConverters; +using Xunit; + +namespace Serilog.Sinks.ApplicationInsights.Tests.TelemetryConverters.Trace; + +public class IncludeOperationIdTest : ApplicationInsightsTest +{ + public IncludeOperationIdTest() + : base(new TraceTelemetryConverter(true, false, false, false), true, true) + { + } + + [Fact] + public void OperationIdIsSetAsTraceProperty() + { + using var activity = new System.Diagnostics.Activity("TestActivity"); + activity.Start(); + + Logger.Information("Hello, {operationId}!", "foo-operation-id"); + + Assert.Equal("foo-operation-id", LastSubmittedTraceTelemetry.Properties["operationId"]); + } +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/IncludeOperationNameTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/IncludeOperationNameTest.cs new file mode 100644 index 0000000..a6e0400 --- /dev/null +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/IncludeOperationNameTest.cs @@ -0,0 +1,23 @@ +using Serilog.Sinks.ApplicationInsights.TelemetryConverters; +using Xunit; + +namespace Serilog.Sinks.ApplicationInsights.Tests.TelemetryConverters.Trace; + +public class IncludeOperationNameTest : ApplicationInsightsTest +{ + public IncludeOperationNameTest() + : base(new TraceTelemetryConverter(false, false, true, false), true, true) + { + } + + [Fact] + public void OperationIdIsSetAsTraceProperty() + { + using var activity = new System.Diagnostics.Activity("TestActivity"); + activity.Start(); + + Logger.Information("Hello, {OperationName}!", "foo-operation-name"); + + Assert.Equal("foo-operation-name", LastSubmittedTraceTelemetry.Properties["OperationName"]); + } +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/IncludeParentSpanIdTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/IncludeParentSpanIdTest.cs new file mode 100644 index 0000000..c863504 --- /dev/null +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/IncludeParentSpanIdTest.cs @@ -0,0 +1,23 @@ +using Serilog.Sinks.ApplicationInsights.TelemetryConverters; +using Xunit; + +namespace Serilog.Sinks.ApplicationInsights.Tests.TelemetryConverters.Trace; + +public class IncludeSpanIdTest : ApplicationInsightsTest +{ + public IncludeSpanIdTest() + : base(new TraceTelemetryConverter(false, true, false, false), true, true) + { + } + + [Fact] + public void OperationIdIsSetAsTraceProperty() + { + using var activity = new System.Diagnostics.Activity("TestActivity"); + activity.Start(); + + Logger.Information("Hello, {ParentSpanId}!", "foo-parent-span-id"); + + Assert.Equal("foo-parent-span-id", LastSubmittedTraceTelemetry.Properties["ParentSpanId"]); + } +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/IncludeVersionTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/IncludeVersionTest.cs new file mode 100644 index 0000000..a2636da --- /dev/null +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/IncludeVersionTest.cs @@ -0,0 +1,23 @@ +using Serilog.Sinks.ApplicationInsights.TelemetryConverters; +using Xunit; + +namespace Serilog.Sinks.ApplicationInsights.Tests.TelemetryConverters.Trace; + +public class IncludeVersionTest : ApplicationInsightsTest +{ + public IncludeVersionTest() + : base(new TraceTelemetryConverter(false, false, false, true), true, true) + { + } + + [Fact] + public void OperationIdIsSetAsTraceProperty() + { + using var activity = new System.Diagnostics.Activity("TestActivity"); + activity.Start(); + + Logger.Information("Hello, {version}!", "v1.3.3.7"); + + Assert.Equal("v1.3.3.7", LastSubmittedTraceTelemetry.Properties["version"]); + } +} diff --git a/test/Serilog.Sinks.ApplicationInsights.Tests/TraceTelemetryConverterTest.cs b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/TraceTelemetryConverterTest.cs similarity index 61% rename from test/Serilog.Sinks.ApplicationInsights.Tests/TraceTelemetryConverterTest.cs rename to test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/TraceTelemetryConverterTest.cs index 7ecd1cc..0140b94 100644 --- a/test/Serilog.Sinks.ApplicationInsights.Tests/TraceTelemetryConverterTest.cs +++ b/test/Serilog.Sinks.ApplicationInsights.Tests/TelemetryConverters/Trace/TraceTelemetryConverterTest.cs @@ -1,13 +1,12 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using Serilog.Sinks.ApplicationInsights.TelemetryConverters; using Xunit; -namespace Serilog.Sinks.ApplicationInsights.Tests; +namespace Serilog.Sinks.ApplicationInsights.Tests.TelemetryConverters.Trace; public class TraceTelemetryConverterTest : ApplicationInsightsTest { - public TraceTelemetryConverterTest() : base(new TraceTelemetryConverter()) + public TraceTelemetryConverterTest() : base(new TraceTelemetryConverter(), true, true) { } @@ -40,21 +39,19 @@ public void DestructuredPropertyIsFormattedCorrectly() } [Fact] - public void TraceIdAndSpanIdDefaultByDefault() + public void TraceIdIsNullByDefault() { Logger.Information("Hello, {Name}!", "world"); Assert.Null(LastSubmittedTraceTelemetry.Context.Operation.Id); - Assert.Null(LastSubmittedTraceTelemetry.Context.Operation.ParentId); } [Fact] - public void TraceIdAndSpanIdAreSet() + public void TraceIdIsSet() { using Activity activity = new("TestActivity"); activity.Start(); Logger.Information("Hello, {Name}!", "world"); Assert.Equal(activity.TraceId.ToHexString(), LastSubmittedTraceTelemetry.Context.Operation.Id); - Assert.Equal(activity.SpanId.ToHexString(), LastSubmittedTraceTelemetry.Context.Operation.ParentId); } [Fact] @@ -67,4 +64,43 @@ public void OperationIdTakesPrecedenceOverTraceId() Assert.Equal(operationId, LastSubmittedTraceTelemetry.Context.Operation.Id); Assert.Null(LastSubmittedTraceTelemetry.Context.Operation.ParentId); } + + [Fact] + public void ParentSpanIdIsSet() + { + Logger.Information("Test {ParentSpanId}", "parent123"); + Assert.Equal("parent123", LastSubmittedTraceTelemetry.Context.Operation.ParentId); + } + + [Fact] + public void VersionIsSet() + { + Logger.Information("Test {version}", "1.2.3"); + Assert.Equal("1.2.3", LastSubmittedTraceTelemetry.Context.Component.Version); + } + + [Fact] + public void OperationNameIsSet() + { + using Activity activity = new("MyOperation"); + activity.Start(); + + Logger.Information("Test"); + + Assert.Equal("MyOperation", LastSubmittedTraceTelemetry.Context.Operation.Name); + } + + [Fact] + public void BaggageIsSet() + { + using Activity activity = new("TestActivity"); + activity.AddBaggage("key1", "value1"); + activity.AddBaggage("key2", "value2"); + activity.Start(); + + Logger.Information("Hello, world!"); + + Assert.Equal("value1", LastSubmittedTraceTelemetry.Properties["key1"]); + Assert.Equal("value2", LastSubmittedTraceTelemetry.Properties["key2"]); + } }