From 83b291f7f3f477712b40f49133ddbe56cc52e7a2 Mon Sep 17 00:00:00 2001 From: Harsimar Kaur Date: Tue, 3 Mar 2026 14:31:23 -0800 Subject: [PATCH 01/15] initial draft for public context --- .../Stable/PublicAPI.Unshipped.txt | 22 ++ .../TelemetryClientContextTagsTest.cs | 271 ++++++++++++++++-- .../DataContracts/TelemetryContext.cs | 28 +- .../Implementation/DeviceContext.cs | 2 +- .../Implementation/OperationContext.cs | 11 +- .../Implementation/SessionContext.cs | 2 +- .../Implementation/UserContext.cs | 2 +- .../Internal/SemanticConventions.cs | 24 ++ .../TelemetryClient.cs | 120 ++++++++ 9 files changed, 433 insertions(+), 49 deletions(-) diff --git a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt index e69de29bb2..44e5ffd585 100644 --- a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt @@ -0,0 +1,22 @@ +Microsoft.ApplicationInsights.DataContracts.TelemetryContext.Device.get -> Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext +Microsoft.ApplicationInsights.DataContracts.TelemetryContext.Session.get -> Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext +Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext +Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.Id.get -> string +Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.Id.set -> void +Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.Model.get -> string +Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.Model.set -> void +Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.OemName.get -> string +Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.OemName.set -> void +Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.OperatingSystem.get -> string +Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.OperatingSystem.set -> void +Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.Type.get -> string +Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.Type.set -> void +Microsoft.ApplicationInsights.Extensibility.Implementation.OperationContext.SyntheticSource.get -> string +Microsoft.ApplicationInsights.Extensibility.Implementation.OperationContext.SyntheticSource.set -> void +Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext +Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext.Id.get -> string +Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext.Id.set -> void +Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext.IsFirst.get -> bool? +Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext.IsFirst.set -> void +Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.AccountId.get -> string +Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.AccountId.set -> void diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs index 3364b55c53..4e529c630e 100644 --- a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs @@ -109,22 +109,38 @@ public void BuildContextTags_LocationIpMapsToMicrosoftClientIp() } [Fact] - public void BuildContextTags_AllPropertiesSet_ContainsAllFiveAttributes() + public void BuildContextTags_AllPropertiesSet_ContainsAllAttributes() { this.telemetryClient.Context.User.Id = "user-1"; this.telemetryClient.Context.User.AuthenticatedUserId = "auth-1"; this.telemetryClient.Context.User.UserAgent = "TestAgent/1.0"; + this.telemetryClient.Context.User.AccountId = "acct-1"; this.telemetryClient.Context.Operation.Name = "TestOp"; + this.telemetryClient.Context.Operation.SyntheticSource = "bot"; this.telemetryClient.Context.Location.Ip = "192.168.1.1"; + this.telemetryClient.Context.Session.Id = "session-1"; + this.telemetryClient.Context.Session.IsFirst = true; + this.telemetryClient.Context.Device.Id = "device-1"; + this.telemetryClient.Context.Device.Model = "Surface Pro"; + this.telemetryClient.Context.Device.OemName = "Microsoft"; + this.telemetryClient.Context.Device.Type = "Tablet"; var tags = this.telemetryClient.ContextTags; - Assert.Equal(5, tags.Count); + Assert.Equal(13, tags.Count); Assert.Equal("user-1", tags["enduser.pseudo.id"]); Assert.Equal("auth-1", tags["enduser.id"]); Assert.Equal("TestAgent/1.0", tags["user_agent.original"]); Assert.Equal("TestOp", tags["microsoft.operation_name"]); Assert.Equal("192.168.1.1", tags["microsoft.client.ip"]); + Assert.Equal("session-1", tags["microsoft.session.id"]); + Assert.Equal("True", tags["ai.session.isFirst"]); + Assert.Equal("device-1", tags["ai.device.id"]); + Assert.Equal("Surface Pro", tags["ai.device.model"]); + Assert.Equal("Microsoft", tags["ai.device.oemName"]); + Assert.Equal("Tablet", tags["ai.device.type"]); + Assert.Equal("bot", tags["microsoft.synthetic_source"]); + Assert.Equal("acct-1", tags["microsoft.user.account_id"]); } #endregion @@ -149,8 +165,15 @@ public void BuildContextTags_EmptyStringExcluded() this.telemetryClient.Context.User.Id = string.Empty; this.telemetryClient.Context.User.AuthenticatedUserId = ""; this.telemetryClient.Context.User.UserAgent = ""; + this.telemetryClient.Context.User.AccountId = ""; this.telemetryClient.Context.Operation.Name = ""; + this.telemetryClient.Context.Operation.SyntheticSource = ""; this.telemetryClient.Context.Location.Ip = ""; + this.telemetryClient.Context.Session.Id = ""; + this.telemetryClient.Context.Device.Id = ""; + this.telemetryClient.Context.Device.Model = ""; + this.telemetryClient.Context.Device.OemName = ""; + this.telemetryClient.Context.Device.Type = ""; var tags = this.telemetryClient.ContextTags; @@ -211,6 +234,47 @@ public void TrackEvent_String_IncludesClientContextTags() Assert.Equal("10.0.0.1", attributes["microsoft.client.ip"]); } + [Fact] + public void TrackEvent_IncludesAllClientContextTags() + { + this.telemetryClient.Context.User.Id = "evt-user"; + this.telemetryClient.Context.User.AuthenticatedUserId = "evt-auth"; + this.telemetryClient.Context.User.UserAgent = "EvtAgent/1.0"; + this.telemetryClient.Context.User.AccountId = "evt-acct"; + this.telemetryClient.Context.Operation.Name = "EvtOp"; + this.telemetryClient.Context.Operation.SyntheticSource = "evt-bot"; + this.telemetryClient.Context.Location.Ip = "10.0.0.2"; + this.telemetryClient.Context.Session.Id = "evt-session"; + this.telemetryClient.Context.Session.IsFirst = true; + this.telemetryClient.Context.Device.Id = "evt-device"; + this.telemetryClient.Context.Device.Model = "Phone"; + this.telemetryClient.Context.Device.OemName = "Samsung"; + this.telemetryClient.Context.Device.Type = "Mobile"; + + this.telemetryClient.TrackEvent("AllContextEvent"); + this.telemetryClient.Flush(); + + var logRecord = this.logItems.FirstOrDefault(l => + l.Attributes != null && l.Attributes.Any(a => + a.Key == "microsoft.custom_event.name" && a.Value?.ToString() == "AllContextEvent")); + Assert.NotNull(logRecord); + + var attributes = logRecord.Attributes.ToDictionary(a => a.Key, a => a.Value?.ToString()); + Assert.Equal("evt-user", attributes["enduser.pseudo.id"]); + Assert.Equal("evt-auth", attributes["enduser.id"]); + Assert.Equal("EvtAgent/1.0", attributes["user_agent.original"]); + Assert.Equal("EvtOp", attributes["microsoft.operation_name"]); + Assert.Equal("10.0.0.2", attributes["microsoft.client.ip"]); + Assert.Equal("evt-session", attributes["microsoft.session.id"]); + Assert.Equal("True", attributes["ai.session.isFirst"]); + Assert.Equal("evt-device", attributes["ai.device.id"]); + Assert.Equal("Phone", attributes["ai.device.model"]); + Assert.Equal("Samsung", attributes["ai.device.oemName"]); + Assert.Equal("Mobile", attributes["ai.device.type"]); + Assert.Equal("evt-bot", attributes["microsoft.synthetic_source"]); + Assert.Equal("evt-acct", attributes["microsoft.user.account_id"]); + } + [Fact] public void TrackTrace_String_IncludesClientContextTags() { @@ -241,10 +305,59 @@ public void TrackTrace_WithSeverityAndProperties_IncludesClientContextTags() } [Fact] - public void TrackException_Exception_IncludesClientContextTags() + public void TrackTrace_IncludesAllClientContextTags() + { + this.telemetryClient.Context.User.Id = "trc-user"; + this.telemetryClient.Context.User.AuthenticatedUserId = "trc-auth"; + this.telemetryClient.Context.User.UserAgent = "TrcAgent/1.0"; + this.telemetryClient.Context.User.AccountId = "trc-acct"; + this.telemetryClient.Context.Operation.Name = "TrcOp"; + this.telemetryClient.Context.Operation.SyntheticSource = "trc-bot"; + this.telemetryClient.Context.Location.Ip = "10.0.0.3"; + this.telemetryClient.Context.Session.Id = "trc-session"; + this.telemetryClient.Context.Session.IsFirst = false; + this.telemetryClient.Context.Device.Id = "trc-device"; + this.telemetryClient.Context.Device.Model = "Desktop"; + this.telemetryClient.Context.Device.OemName = "HP"; + this.telemetryClient.Context.Device.Type = "PC"; + + this.telemetryClient.TrackTrace("AllContextTrace"); + this.telemetryClient.Flush(); + + Assert.True(this.logItems.Count > 0); + var logRecord = this.logItems.Last(); + var attributes = logRecord.Attributes.ToDictionary(a => a.Key, a => a.Value?.ToString()); + Assert.Equal("trc-user", attributes["enduser.pseudo.id"]); + Assert.Equal("trc-auth", attributes["enduser.id"]); + Assert.Equal("TrcAgent/1.0", attributes["user_agent.original"]); + Assert.Equal("TrcOp", attributes["microsoft.operation_name"]); + Assert.Equal("10.0.0.3", attributes["microsoft.client.ip"]); + Assert.Equal("trc-session", attributes["microsoft.session.id"]); + Assert.Equal("False", attributes["ai.session.isFirst"]); + Assert.Equal("trc-device", attributes["ai.device.id"]); + Assert.Equal("Desktop", attributes["ai.device.model"]); + Assert.Equal("HP", attributes["ai.device.oemName"]); + Assert.Equal("PC", attributes["ai.device.type"]); + Assert.Equal("trc-bot", attributes["microsoft.synthetic_source"]); + Assert.Equal("trc-acct", attributes["microsoft.user.account_id"]); + } + + [Fact] + public void TrackException_IncludesAllClientContextTags() { this.telemetryClient.Context.User.Id = "exc-user"; + this.telemetryClient.Context.User.AuthenticatedUserId = "exc-auth"; + this.telemetryClient.Context.User.UserAgent = "ExcAgent/1.0"; + this.telemetryClient.Context.User.AccountId = "exc-acct"; + this.telemetryClient.Context.Operation.Name = "ExcOp"; + this.telemetryClient.Context.Operation.SyntheticSource = "exc-bot"; this.telemetryClient.Context.Location.Ip = "172.16.0.1"; + this.telemetryClient.Context.Session.Id = "exc-session"; + this.telemetryClient.Context.Session.IsFirst = true; + this.telemetryClient.Context.Device.Id = "exc-device"; + this.telemetryClient.Context.Device.Model = "Tablet"; + this.telemetryClient.Context.Device.OemName = "Asus"; + this.telemetryClient.Context.Device.Type = "Portable"; this.telemetryClient.TrackException(new InvalidOperationException("boom")); this.telemetryClient.Flush(); @@ -253,7 +366,18 @@ public void TrackException_Exception_IncludesClientContextTags() Assert.NotNull(logRecord); var attributes = logRecord.Attributes.ToDictionary(a => a.Key, a => a.Value?.ToString()); Assert.Equal("exc-user", attributes["enduser.pseudo.id"]); + Assert.Equal("exc-auth", attributes["enduser.id"]); + Assert.Equal("ExcAgent/1.0", attributes["user_agent.original"]); + Assert.Equal("ExcOp", attributes["microsoft.operation_name"]); Assert.Equal("172.16.0.1", attributes["microsoft.client.ip"]); + Assert.Equal("exc-session", attributes["microsoft.session.id"]); + Assert.Equal("True", attributes["ai.session.isFirst"]); + Assert.Equal("exc-device", attributes["ai.device.id"]); + Assert.Equal("Tablet", attributes["ai.device.model"]); + Assert.Equal("Asus", attributes["ai.device.oemName"]); + Assert.Equal("Portable", attributes["ai.device.type"]); + Assert.Equal("exc-bot", attributes["microsoft.synthetic_source"]); + Assert.Equal("exc-acct", attributes["microsoft.user.account_id"]); } [Fact] @@ -444,11 +568,19 @@ public void TrackEvent_AllTiersMergeWithNoKeyOverlap() #region Activity-based Telemetry — Context Tags Applied [Fact] - public void TrackRequest_IncludesClientContextTags() + public void TrackRequest_IncludesAllClientContextTags() { this.telemetryClient.Context.User.Id = "req-user"; this.telemetryClient.Context.User.AuthenticatedUserId = "req-auth"; + this.telemetryClient.Context.User.AccountId = "req-acct"; this.telemetryClient.Context.Location.Ip = "10.0.0.1"; + this.telemetryClient.Context.Session.Id = "req-session"; + this.telemetryClient.Context.Session.IsFirst = true; + this.telemetryClient.Context.Device.Id = "req-device"; + this.telemetryClient.Context.Device.Model = "Laptop"; + this.telemetryClient.Context.Device.OemName = "Dell"; + this.telemetryClient.Context.Device.Type = "PC"; + this.telemetryClient.Context.Operation.SyntheticSource = "test-runner"; var request = new RequestTelemetry("GET /api", DateTimeOffset.UtcNow, TimeSpan.FromMilliseconds(50), "200", true); this.telemetryClient.TrackRequest(request); @@ -459,13 +591,32 @@ public void TrackRequest_IncludesClientContextTags() Assert.True(activity.Tags.Any(t => t.Key == "enduser.pseudo.id" && t.Value == "req-user")); Assert.True(activity.Tags.Any(t => t.Key == "enduser.id" && t.Value == "req-auth")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.client.ip" && t.Value == "10.0.0.1")); + Assert.True(activity.Tags.Any(t => t.Key == "microsoft.session.id" && t.Value == "req-session")); + Assert.True(activity.Tags.Any(t => t.Key == "ai.session.isFirst" && t.Value == "True")); + Assert.True(activity.Tags.Any(t => t.Key == "ai.device.id" && t.Value == "req-device")); + Assert.True(activity.Tags.Any(t => t.Key == "ai.device.model" && t.Value == "Laptop")); + Assert.True(activity.Tags.Any(t => t.Key == "ai.device.oemName" && t.Value == "Dell")); + Assert.True(activity.Tags.Any(t => t.Key == "ai.device.type" && t.Value == "PC")); + Assert.True(activity.Tags.Any(t => t.Key == "microsoft.synthetic_source" && t.Value == "test-runner")); + Assert.True(activity.Tags.Any(t => t.Key == "microsoft.user.account_id" && t.Value == "req-acct")); } [Fact] - public void TrackDependency_IncludesClientContextTags() + public void TrackDependency_IncludesAllClientContextTags() { this.telemetryClient.Context.User.Id = "dep-user"; + this.telemetryClient.Context.User.AuthenticatedUserId = "dep-auth"; + this.telemetryClient.Context.User.UserAgent = "DepAgent/1.0"; + this.telemetryClient.Context.User.AccountId = "dep-acct"; this.telemetryClient.Context.Operation.Name = "ParentOp"; + this.telemetryClient.Context.Operation.SyntheticSource = "dep-bot"; + this.telemetryClient.Context.Location.Ip = "10.0.0.4"; + this.telemetryClient.Context.Session.Id = "dep-session"; + this.telemetryClient.Context.Session.IsFirst = true; + this.telemetryClient.Context.Device.Id = "dep-device"; + this.telemetryClient.Context.Device.Model = "Watch"; + this.telemetryClient.Context.Device.OemName = "Apple"; + this.telemetryClient.Context.Device.Type = "Wearable"; var dep = new DependencyTelemetry { @@ -480,7 +631,18 @@ public void TrackDependency_IncludesClientContextTags() Assert.Equal(1, this.activityItems.Count); var activity = this.activityItems[0]; Assert.True(activity.Tags.Any(t => t.Key == "enduser.pseudo.id" && t.Value == "dep-user")); + Assert.True(activity.Tags.Any(t => t.Key == "enduser.id" && t.Value == "dep-auth")); + Assert.True(activity.Tags.Any(t => t.Key == "user_agent.original" && t.Value == "DepAgent/1.0")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.operation_name" && t.Value == "ParentOp")); + Assert.True(activity.Tags.Any(t => t.Key == "microsoft.client.ip" && t.Value == "10.0.0.4")); + Assert.True(activity.Tags.Any(t => t.Key == "microsoft.session.id" && t.Value == "dep-session")); + Assert.True(activity.Tags.Any(t => t.Key == "ai.session.isFirst" && t.Value == "True")); + Assert.True(activity.Tags.Any(t => t.Key == "ai.device.id" && t.Value == "dep-device")); + Assert.True(activity.Tags.Any(t => t.Key == "ai.device.model" && t.Value == "Watch")); + Assert.True(activity.Tags.Any(t => t.Key == "ai.device.oemName" && t.Value == "Apple")); + Assert.True(activity.Tags.Any(t => t.Key == "ai.device.type" && t.Value == "Wearable")); + Assert.True(activity.Tags.Any(t => t.Key == "microsoft.synthetic_source" && t.Value == "dep-bot")); + Assert.True(activity.Tags.Any(t => t.Key == "microsoft.user.account_id" && t.Value == "dep-acct")); } #endregion @@ -594,31 +756,50 @@ public void TrackDependency_ItemOperationNameOverridesClientOperationName() #region Metric Telemetry — Context Tags Applied [Fact] - public void TrackMetric_String_IncludesClientContextTags() + public void TrackMetric_IncludesAllClientContextTags() { this.telemetryClient.Context.User.Id = "metric-user"; + this.telemetryClient.Context.User.AuthenticatedUserId = "metric-auth"; + this.telemetryClient.Context.User.UserAgent = "MetricAgent/1.0"; + this.telemetryClient.Context.User.AccountId = "metric-acct"; + this.telemetryClient.Context.Operation.Name = "MetricOp"; + this.telemetryClient.Context.Operation.SyntheticSource = "metric-bot"; this.telemetryClient.Context.Location.Ip = "10.0.0.5"; - - this.telemetryClient.TrackMetric("TestMetric", 42.0); + this.telemetryClient.Context.Session.Id = "metric-session"; + this.telemetryClient.Context.Session.IsFirst = true; + this.telemetryClient.Context.Device.Id = "metric-device"; + this.telemetryClient.Context.Device.Model = "Server"; + this.telemetryClient.Context.Device.OemName = "Lenovo"; + this.telemetryClient.Context.Device.Type = "Rack"; + + this.telemetryClient.TrackMetric("AllContextMetric", 42.0); this.telemetryClient.Flush(); Assert.True(this.metricItems.Count > 0); - var metric = this.metricItems.FirstOrDefault(m => m.Name == "TestMetric"); + var metric = this.metricItems.FirstOrDefault(m => m.Name == "AllContextMetric"); Assert.NotNull(metric); foreach (var point in metric.GetMetricPoints()) { - bool hasUserId = false; - bool hasIp = false; + var tagDict = new Dictionary(); foreach (var tag in point.Tags) { - if (tag.Key == "enduser.pseudo.id" && tag.Value?.ToString() == "metric-user") - hasUserId = true; - if (tag.Key == "microsoft.client.ip" && tag.Value?.ToString() == "10.0.0.5") - hasIp = true; + tagDict[tag.Key] = tag.Value?.ToString(); } - Assert.True(hasUserId, "Context tag enduser.pseudo.id should be on metric"); - Assert.True(hasIp, "Context tag microsoft.client.ip should be on metric"); + + Assert.Equal("metric-user", tagDict["enduser.pseudo.id"]); + Assert.Equal("metric-auth", tagDict["enduser.id"]); + Assert.Equal("MetricAgent/1.0", tagDict["user_agent.original"]); + Assert.Equal("MetricOp", tagDict["microsoft.operation_name"]); + Assert.Equal("10.0.0.5", tagDict["microsoft.client.ip"]); + Assert.Equal("metric-session", tagDict["microsoft.session.id"]); + Assert.Equal("True", tagDict["ai.session.isFirst"]); + Assert.Equal("metric-device", tagDict["ai.device.id"]); + Assert.Equal("Server", tagDict["ai.device.model"]); + Assert.Equal("Lenovo", tagDict["ai.device.oemName"]); + Assert.Equal("Rack", tagDict["ai.device.type"]); + Assert.Equal("metric-bot", tagDict["microsoft.synthetic_source"]); + Assert.Equal("metric-acct", tagDict["microsoft.user.account_id"]); break; } } @@ -660,27 +841,51 @@ public void TrackMetric_PropertiesOverrideContextTags() } [Fact] - public void GetMetric_TrackValue_ZeroDimensions_IncludesContextTags() + public void GetMetric_TrackValue_IncludesAllClientContextTags() { this.telemetryClient.Context.User.Id = "getmetric-user"; - - var metric = this.telemetryClient.GetMetric("ZeroDimMetric"); + this.telemetryClient.Context.User.AuthenticatedUserId = "getmetric-auth"; + this.telemetryClient.Context.User.UserAgent = "GetMetricAgent/1.0"; + this.telemetryClient.Context.User.AccountId = "getmetric-acct"; + this.telemetryClient.Context.Operation.Name = "GetMetricOp"; + this.telemetryClient.Context.Operation.SyntheticSource = "getmetric-bot"; + this.telemetryClient.Context.Location.Ip = "10.0.0.6"; + this.telemetryClient.Context.Session.Id = "getmetric-session"; + this.telemetryClient.Context.Session.IsFirst = true; + this.telemetryClient.Context.Device.Id = "getmetric-device"; + this.telemetryClient.Context.Device.Model = "VM"; + this.telemetryClient.Context.Device.OemName = "Azure"; + this.telemetryClient.Context.Device.Type = "Cloud"; + + var metric = this.telemetryClient.GetMetric("AllContextGetMetric"); metric.TrackValue(5.0); this.telemetryClient.Flush(); - var collected = this.metricItems.FirstOrDefault(m => m.Name == "ZeroDimMetric"); + var collected = this.metricItems.FirstOrDefault(m => m.Name == "AllContextGetMetric"); Assert.NotNull(collected); foreach (var point in collected.GetMetricPoints()) { - bool hasUserId = false; + var tagDict = new Dictionary(); foreach (var tag in point.Tags) { - if (tag.Key == "enduser.pseudo.id" && tag.Value?.ToString() == "getmetric-user") - hasUserId = true; + tagDict[tag.Key] = tag.Value?.ToString(); } - Assert.True(hasUserId, "Context tag should appear on GetMetric().TrackValue()"); + + Assert.Equal("getmetric-user", tagDict["enduser.pseudo.id"]); + Assert.Equal("getmetric-auth", tagDict["enduser.id"]); + Assert.Equal("GetMetricAgent/1.0", tagDict["user_agent.original"]); + Assert.Equal("GetMetricOp", tagDict["microsoft.operation_name"]); + Assert.Equal("10.0.0.6", tagDict["microsoft.client.ip"]); + Assert.Equal("getmetric-session", tagDict["microsoft.session.id"]); + Assert.Equal("True", tagDict["ai.session.isFirst"]); + Assert.Equal("getmetric-device", tagDict["ai.device.id"]); + Assert.Equal("VM", tagDict["ai.device.model"]); + Assert.Equal("Azure", tagDict["ai.device.oemName"]); + Assert.Equal("Cloud", tagDict["ai.device.type"]); + Assert.Equal("getmetric-bot", tagDict["microsoft.synthetic_source"]); + Assert.Equal("getmetric-acct", tagDict["microsoft.user.account_id"]); break; } } @@ -772,6 +977,14 @@ public void TrackEvent_NoContextSet_EmitsWithoutError() Assert.False(attributes.ContainsKey("user_agent.original")); Assert.False(attributes.ContainsKey("microsoft.operation_name")); Assert.False(attributes.ContainsKey("microsoft.client.ip")); + Assert.False(attributes.ContainsKey("microsoft.session.id")); + Assert.False(attributes.ContainsKey("ai.session.isFirst")); + Assert.False(attributes.ContainsKey("ai.device.id")); + Assert.False(attributes.ContainsKey("ai.device.model")); + Assert.False(attributes.ContainsKey("ai.device.oemName")); + Assert.False(attributes.ContainsKey("ai.device.type")); + Assert.False(attributes.ContainsKey("microsoft.synthetic_source")); + Assert.False(attributes.ContainsKey("microsoft.user.account_id")); } [Fact] @@ -825,6 +1038,14 @@ public void TrackRequest_NoContextSet_EmitsWithoutContextTags() Assert.False(activity.Tags.Any(t => t.Key == "enduser.id")); Assert.False(activity.Tags.Any(t => t.Key == "user_agent.original")); Assert.False(activity.Tags.Any(t => t.Key == "microsoft.client.ip")); + Assert.False(activity.Tags.Any(t => t.Key == "microsoft.session.id")); + Assert.False(activity.Tags.Any(t => t.Key == "ai.session.isFirst")); + Assert.False(activity.Tags.Any(t => t.Key == "ai.device.id")); + Assert.False(activity.Tags.Any(t => t.Key == "ai.device.model")); + Assert.False(activity.Tags.Any(t => t.Key == "ai.device.oemName")); + Assert.False(activity.Tags.Any(t => t.Key == "ai.device.type")); + Assert.False(activity.Tags.Any(t => t.Key == "microsoft.synthetic_source")); + Assert.False(activity.Tags.Any(t => t.Key == "microsoft.user.account_id")); } [Fact] diff --git a/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs b/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs index 606c37878b..345d89f54f 100644 --- a/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs @@ -77,37 +77,37 @@ public LocationContext Location } /// - /// Gets the object describing the cloud tracked by this . + /// Gets the object describing the device tracked by this . /// - internal CloudContext Cloud + public DeviceContext Device { - get { return LazyInitializer.EnsureInitialized(ref this.cloud, () => new CloudContext()); } +#pragma warning disable CS0618 // Type or member is obsolete + get { return LazyInitializer.EnsureInitialized(ref this.device, () => new DeviceContext(default)); } +#pragma warning restore CS0618 // Type or member is obsolete } /// - /// Gets the object describing the component tracked by this . + /// Gets the object describing a user session tracked by this . /// - internal ComponentContext Component + public SessionContext Session { - get { return LazyInitializer.EnsureInitialized(ref this.component, () => new ComponentContext()); } + get { return LazyInitializer.EnsureInitialized(ref this.session, () => new SessionContext()); } } /// - /// Gets the object describing the device tracked by this . + /// Gets the object describing the cloud tracked by this . /// - internal DeviceContext Device + internal CloudContext Cloud { -#pragma warning disable CS0618 // Type or member is obsolete - get { return LazyInitializer.EnsureInitialized(ref this.device, () => new DeviceContext(default)); } -#pragma warning restore CS0618 // Type or member is obsolete + get { return LazyInitializer.EnsureInitialized(ref this.cloud, () => new CloudContext()); } } /// - /// Gets the object describing a user session tracked by this . + /// Gets the object describing the component tracked by this . /// - internal SessionContext Session + internal ComponentContext Component { - get { return LazyInitializer.EnsureInitialized(ref this.session, () => new SessionContext()); } + get { return LazyInitializer.EnsureInitialized(ref this.component, () => new ComponentContext()); } } } } \ No newline at end of file diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/DeviceContext.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/DeviceContext.cs index 8ed78cc86d..fe50903339 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/DeviceContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/DeviceContext.cs @@ -5,7 +5,7 @@ /// /// Encapsulates information about a device where an application is running. /// - internal sealed class DeviceContext + public sealed class DeviceContext { private readonly IDictionary properties; diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/OperationContext.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/OperationContext.cs index 6ede0b7330..9c64e8e1ad 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/OperationContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/OperationContext.cs @@ -1,7 +1,5 @@ namespace Microsoft.ApplicationInsights.Extensibility.Implementation { - using System.ComponentModel; - /// /// Encapsulates information about an operation. Operation normally reflects an end to end scenario that starts from a user action (e.g. button click). /// @@ -29,14 +27,14 @@ public string Name /// /// Gets or sets the application-defined operation SyntheticSource. /// - internal string SyntheticSource + public string SyntheticSource { get { return string.IsNullOrEmpty(this.syntheticSource) ? null : this.syntheticSource; } set { this.syntheticSource = value; } } /// - /// Gets or sets the application-defined operation ID for the topmost operation. + /// Gets or sets the application-defined operation ID. /// internal string Id { @@ -45,7 +43,7 @@ internal string Id } /// - /// Gets or sets the parent operation ID. + /// Gets or sets the application-defined parent operation ID. /// internal string ParentId { @@ -54,9 +52,8 @@ internal string ParentId } /// - /// Gets or sets the correlation vector for the current telemetry item. + /// Gets or sets the application-defined correlation vector. /// - [EditorBrowsable(EditorBrowsableState.Never)] internal string CorrelationVector { get { return string.IsNullOrEmpty(this.correlationVector) ? null : this.correlationVector; } diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/SessionContext.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/SessionContext.cs index 9f9402f710..49d82c1946 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/SessionContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/SessionContext.cs @@ -3,7 +3,7 @@ /// /// Encapsulates information about a user session. /// - internal sealed class SessionContext + public sealed class SessionContext { private string id; private bool? isFirst; diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs index 27d03d0ab2..7eae54e7ea 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs @@ -48,7 +48,7 @@ public string AuthenticatedUserId /// /// Gets or sets the ID of an application-defined account associated with the user. /// - internal string AccountId + public string AccountId { get { return string.IsNullOrEmpty(this.accountId) ? null : this.accountId; } set { this.accountId = value; } diff --git a/BASE/src/Microsoft.ApplicationInsights/Internal/SemanticConventions.cs b/BASE/src/Microsoft.ApplicationInsights/Internal/SemanticConventions.cs index ce12e1adf3..82160fc85d 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Internal/SemanticConventions.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Internal/SemanticConventions.cs @@ -231,5 +231,29 @@ internal static class SemanticConventions /// When present, maps to ai.location.ip context tag. /// public const string AttributeMicrosoftClientIp = "microsoft.client.ip"; + + /// Attribute for session ID. + public const string AttributeMicrosoftSessionId = "microsoft.session.id"; + + /// Attribute for first session flag. + public const string AttributeAiSessionIsFirst = "ai.session.isFirst"; + + /// Attribute for device ID. + public const string AttributeAiDeviceId = "ai.device.id"; + + /// Attribute for device model. + public const string AttributeAiDeviceModel = "ai.device.model"; + + /// Attribute for device OEM name. + public const string AttributeAiDeviceOemName = "ai.device.oemName"; + + /// Attribute for device type. + public const string AttributeAiDeviceType = "ai.device.type"; + + /// Attribute for synthetic source. + public const string AttributeMicrosoftSyntheticSource = "microsoft.synthetic_source"; + + /// Attribute for user account ID. + public const string AttributeMicrosoftUserAccountId = "microsoft.user.account_id"; } } \ No newline at end of file diff --git a/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs b/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs index 8fad6e20a9..3ca37d2797 100644 --- a/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs +++ b/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs @@ -1460,6 +1460,46 @@ private static void ApplyContextToActivity(TelemetryContext context, Activity ac { activity.SetTag(SemanticConventions.AttributeMicrosoftClientIp, context.Location.Ip); } + + if (!string.IsNullOrEmpty(context.Session?.Id)) + { + activity.SetTag(SemanticConventions.AttributeMicrosoftSessionId, context.Session.Id); + } + + if (context.Session?.IsFirst != null) + { + activity.SetTag(SemanticConventions.AttributeAiSessionIsFirst, context.Session.IsFirst.Value.ToString()); + } + + if (!string.IsNullOrEmpty(context.Device?.Id)) + { + activity.SetTag(SemanticConventions.AttributeAiDeviceId, context.Device.Id); + } + + if (!string.IsNullOrEmpty(context.Device?.Model)) + { + activity.SetTag(SemanticConventions.AttributeAiDeviceModel, context.Device.Model); + } + + if (!string.IsNullOrEmpty(context.Device?.OemName)) + { + activity.SetTag(SemanticConventions.AttributeAiDeviceOemName, context.Device.OemName); + } + + if (!string.IsNullOrEmpty(context.Device?.Type)) + { + activity.SetTag(SemanticConventions.AttributeAiDeviceType, context.Device.Type); + } + + if (!string.IsNullOrEmpty(context.Operation?.SyntheticSource)) + { + activity.SetTag(SemanticConventions.AttributeMicrosoftSyntheticSource, context.Operation.SyntheticSource); + } + + if (!string.IsNullOrEmpty(context.User?.AccountId)) + { + activity.SetTag(SemanticConventions.AttributeMicrosoftUserAccountId, context.User.AccountId); + } } /// @@ -1498,6 +1538,46 @@ private static void ApplyContextToProperties(TelemetryContext context, IDictiona { properties[SemanticConventions.AttributeMicrosoftClientIp] = context.Location.Ip; } + + if (!string.IsNullOrEmpty(context.Session?.Id)) + { + properties[SemanticConventions.AttributeMicrosoftSessionId] = context.Session.Id; + } + + if (context.Session?.IsFirst != null) + { + properties[SemanticConventions.AttributeAiSessionIsFirst] = context.Session.IsFirst.Value.ToString(); + } + + if (!string.IsNullOrEmpty(context.Device?.Id)) + { + properties[SemanticConventions.AttributeAiDeviceId] = context.Device.Id; + } + + if (!string.IsNullOrEmpty(context.Device?.Model)) + { + properties[SemanticConventions.AttributeAiDeviceModel] = context.Device.Model; + } + + if (!string.IsNullOrEmpty(context.Device?.OemName)) + { + properties[SemanticConventions.AttributeAiDeviceOemName] = context.Device.OemName; + } + + if (!string.IsNullOrEmpty(context.Device?.Type)) + { + properties[SemanticConventions.AttributeAiDeviceType] = context.Device.Type; + } + + if (!string.IsNullOrEmpty(context.Operation?.SyntheticSource)) + { + properties[SemanticConventions.AttributeMicrosoftSyntheticSource] = context.Operation.SyntheticSource; + } + + if (!string.IsNullOrEmpty(context.User?.AccountId)) + { + properties[SemanticConventions.AttributeMicrosoftUserAccountId] = context.User.AccountId; + } } /// @@ -1552,6 +1632,46 @@ private Dictionary BuildContextTags() tags[SemanticConventions.AttributeMicrosoftClientIp] = this.Context.Location.Ip; } + if (!string.IsNullOrEmpty(this.Context.Session?.Id)) + { + tags[SemanticConventions.AttributeMicrosoftSessionId] = this.Context.Session.Id; + } + + if (this.Context.Session?.IsFirst != null) + { + tags[SemanticConventions.AttributeAiSessionIsFirst] = this.Context.Session.IsFirst.Value.ToString(); + } + + if (!string.IsNullOrEmpty(this.Context.Device?.Id)) + { + tags[SemanticConventions.AttributeAiDeviceId] = this.Context.Device.Id; + } + + if (!string.IsNullOrEmpty(this.Context.Device?.Model)) + { + tags[SemanticConventions.AttributeAiDeviceModel] = this.Context.Device.Model; + } + + if (!string.IsNullOrEmpty(this.Context.Device?.OemName)) + { + tags[SemanticConventions.AttributeAiDeviceOemName] = this.Context.Device.OemName; + } + + if (!string.IsNullOrEmpty(this.Context.Device?.Type)) + { + tags[SemanticConventions.AttributeAiDeviceType] = this.Context.Device.Type; + } + + if (!string.IsNullOrEmpty(this.Context.Operation?.SyntheticSource)) + { + tags[SemanticConventions.AttributeMicrosoftSyntheticSource] = this.Context.Operation.SyntheticSource; + } + + if (!string.IsNullOrEmpty(this.Context.User?.AccountId)) + { + tags[SemanticConventions.AttributeMicrosoftUserAccountId] = this.Context.User.AccountId; + } + return tags; } From 1b65fa9dda0d61c246b42d228427af7795ef42d7 Mon Sep 17 00:00:00 2001 From: Harsimar Kaur Date: Wed, 4 Mar 2026 10:00:03 -0800 Subject: [PATCH 02/15] component context --- .../Stable/PublicAPI.Unshipped.txt | 4 ++++ .../TelemetryClientTest.cs | 14 +++++++++++++ .../DataContracts/TelemetryContext.cs | 12 +++++------ .../Implementation/ComponentContext.cs | 21 ++++++++++++++++--- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt index 44e5ffd585..78547aeed0 100644 --- a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt @@ -20,3 +20,7 @@ Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext.IsFirs Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext.IsFirst.set -> void Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.AccountId.get -> string Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.AccountId.set -> void +Microsoft.ApplicationInsights.DataContracts.TelemetryContext.Component.get -> Microsoft.ApplicationInsights.Extensibility.Implementation.ComponentContext +Microsoft.ApplicationInsights.Extensibility.Implementation.ComponentContext +Microsoft.ApplicationInsights.Extensibility.Implementation.ComponentContext.Version.get -> string +Microsoft.ApplicationInsights.Extensibility.Implementation.ComponentContext.Version.set -> void diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientTest.cs b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientTest.cs index 8a1e5d9c30..cf184449fe 100644 --- a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientTest.cs +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientTest.cs @@ -2169,6 +2169,20 @@ public void CloudContextRoleNameSetsEnvironmentVariable() } } + [Fact] + public void ComponentContextVersionSetsEnvironmentVariable() + { + try + { + this.telemetryClient.Context.Component.Version = "1.2.3"; + Assert.Equal("1.2.3", Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_COMPONENT_VERSION")); + } + finally + { + Environment.SetEnvironmentVariable("APPLICATIONINSIGHTS_COMPONENT_VERSION", null); + } + } + private double ComputeSomethingHeavy() { var random = new Random(); diff --git a/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs b/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs index 345d89f54f..4f9c245840 100644 --- a/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs @@ -95,19 +95,19 @@ public SessionContext Session } /// - /// Gets the object describing the cloud tracked by this . + /// Gets the object describing the component tracked by this . /// - internal CloudContext Cloud + public ComponentContext Component { - get { return LazyInitializer.EnsureInitialized(ref this.cloud, () => new CloudContext()); } + get { return LazyInitializer.EnsureInitialized(ref this.component, () => new ComponentContext()); } } /// - /// Gets the object describing the component tracked by this . + /// Gets the object describing the cloud tracked by this . /// - internal ComponentContext Component + internal CloudContext Cloud { - get { return LazyInitializer.EnsureInitialized(ref this.component, () => new ComponentContext()); } + get { return LazyInitializer.EnsureInitialized(ref this.cloud, () => new CloudContext()); } } } } \ No newline at end of file diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/ComponentContext.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/ComponentContext.cs index c662738a07..ca3ef9111c 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/ComponentContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/ComponentContext.cs @@ -1,5 +1,7 @@ namespace Microsoft.ApplicationInsights.Extensibility.Implementation { + using System; + /// /// Encapsulates information describing an Application Insights component. /// @@ -8,8 +10,13 @@ /// with terminology used by our portal and services and to encourage standardization of terminology within our /// organization. Once a consensus is reached, we will change type and property names to match. /// - internal sealed class ComponentContext + public sealed class ComponentContext { + /// + /// Environment variable key used to communicate component version to the exporter. + /// + internal const string ComponentVersionEnvironmentVariable = "APPLICATIONINSIGHTS_COMPONENT_VERSION"; + private string version; internal ComponentContext() @@ -21,8 +28,16 @@ internal ComponentContext() /// public string Version { - get { return string.IsNullOrEmpty(this.version) ? null : this.version; } - set { this.version = value; } + get + { + return string.IsNullOrEmpty(this.version) ? null : this.version; + } + + set + { + this.version = value; + Environment.SetEnvironmentVariable(ComponentVersionEnvironmentVariable, value); + } } } } From 142654605385156a3247a8758875fd99697a199f Mon Sep 17 00:00:00 2001 From: Harsimar Kaur Date: Thu, 5 Mar 2026 14:33:51 -0800 Subject: [PATCH 03/15] add device version --- .../TelemetryClientContextTagsTest.cs | 15 ++++++++++++++- .../Internal/SemanticConventions.cs | 3 +++ .../TelemetryClient.cs | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs index 4e529c630e..1a227f74bf 100644 --- a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs @@ -124,10 +124,11 @@ public void BuildContextTags_AllPropertiesSet_ContainsAllAttributes() this.telemetryClient.Context.Device.Model = "Surface Pro"; this.telemetryClient.Context.Device.OemName = "Microsoft"; this.telemetryClient.Context.Device.Type = "Tablet"; + this.telemetryClient.Context.Device.OperatingSystem = "Windows 11"; var tags = this.telemetryClient.ContextTags; - Assert.Equal(13, tags.Count); + Assert.Equal(14, tags.Count); Assert.Equal("user-1", tags["enduser.pseudo.id"]); Assert.Equal("auth-1", tags["enduser.id"]); Assert.Equal("TestAgent/1.0", tags["user_agent.original"]); @@ -139,6 +140,7 @@ public void BuildContextTags_AllPropertiesSet_ContainsAllAttributes() Assert.Equal("Surface Pro", tags["ai.device.model"]); Assert.Equal("Microsoft", tags["ai.device.oemName"]); Assert.Equal("Tablet", tags["ai.device.type"]); + Assert.Equal("Windows 11", tags["ai.device.osVersion"]); Assert.Equal("bot", tags["microsoft.synthetic_source"]); Assert.Equal("acct-1", tags["microsoft.user.account_id"]); } @@ -174,6 +176,7 @@ public void BuildContextTags_EmptyStringExcluded() this.telemetryClient.Context.Device.Model = ""; this.telemetryClient.Context.Device.OemName = ""; this.telemetryClient.Context.Device.Type = ""; + this.telemetryClient.Context.Device.OperatingSystem = ""; var tags = this.telemetryClient.ContextTags; @@ -250,6 +253,7 @@ public void TrackEvent_IncludesAllClientContextTags() this.telemetryClient.Context.Device.Model = "Phone"; this.telemetryClient.Context.Device.OemName = "Samsung"; this.telemetryClient.Context.Device.Type = "Mobile"; + this.telemetryClient.Context.Device.OperatingSystem = "Android 14"; this.telemetryClient.TrackEvent("AllContextEvent"); this.telemetryClient.Flush(); @@ -271,6 +275,7 @@ public void TrackEvent_IncludesAllClientContextTags() Assert.Equal("Phone", attributes["ai.device.model"]); Assert.Equal("Samsung", attributes["ai.device.oemName"]); Assert.Equal("Mobile", attributes["ai.device.type"]); + Assert.Equal("Android 14", attributes["ai.device.osVersion"]); Assert.Equal("evt-bot", attributes["microsoft.synthetic_source"]); Assert.Equal("evt-acct", attributes["microsoft.user.account_id"]); } @@ -320,6 +325,7 @@ public void TrackTrace_IncludesAllClientContextTags() this.telemetryClient.Context.Device.Model = "Desktop"; this.telemetryClient.Context.Device.OemName = "HP"; this.telemetryClient.Context.Device.Type = "PC"; + this.telemetryClient.Context.Device.OperatingSystem = "Windows 10"; this.telemetryClient.TrackTrace("AllContextTrace"); this.telemetryClient.Flush(); @@ -338,6 +344,7 @@ public void TrackTrace_IncludesAllClientContextTags() Assert.Equal("Desktop", attributes["ai.device.model"]); Assert.Equal("HP", attributes["ai.device.oemName"]); Assert.Equal("PC", attributes["ai.device.type"]); + Assert.Equal("Windows 10", attributes["ai.device.osVersion"]); Assert.Equal("trc-bot", attributes["microsoft.synthetic_source"]); Assert.Equal("trc-acct", attributes["microsoft.user.account_id"]); } @@ -358,6 +365,7 @@ public void TrackException_IncludesAllClientContextTags() this.telemetryClient.Context.Device.Model = "Tablet"; this.telemetryClient.Context.Device.OemName = "Asus"; this.telemetryClient.Context.Device.Type = "Portable"; + this.telemetryClient.Context.Device.OperatingSystem = "Linux 6.1"; this.telemetryClient.TrackException(new InvalidOperationException("boom")); this.telemetryClient.Flush(); @@ -376,6 +384,7 @@ public void TrackException_IncludesAllClientContextTags() Assert.Equal("Tablet", attributes["ai.device.model"]); Assert.Equal("Asus", attributes["ai.device.oemName"]); Assert.Equal("Portable", attributes["ai.device.type"]); + Assert.Equal("Linux 6.1", attributes["ai.device.osVersion"]); Assert.Equal("exc-bot", attributes["microsoft.synthetic_source"]); Assert.Equal("exc-acct", attributes["microsoft.user.account_id"]); } @@ -580,6 +589,7 @@ public void TrackRequest_IncludesAllClientContextTags() this.telemetryClient.Context.Device.Model = "Laptop"; this.telemetryClient.Context.Device.OemName = "Dell"; this.telemetryClient.Context.Device.Type = "PC"; + this.telemetryClient.Context.Device.OperatingSystem = "macOS 14"; this.telemetryClient.Context.Operation.SyntheticSource = "test-runner"; var request = new RequestTelemetry("GET /api", DateTimeOffset.UtcNow, TimeSpan.FromMilliseconds(50), "200", true); @@ -597,6 +607,7 @@ public void TrackRequest_IncludesAllClientContextTags() Assert.True(activity.Tags.Any(t => t.Key == "ai.device.model" && t.Value == "Laptop")); Assert.True(activity.Tags.Any(t => t.Key == "ai.device.oemName" && t.Value == "Dell")); Assert.True(activity.Tags.Any(t => t.Key == "ai.device.type" && t.Value == "PC")); + Assert.True(activity.Tags.Any(t => t.Key == "ai.device.osVersion" && t.Value == "macOS 14")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.synthetic_source" && t.Value == "test-runner")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.user.account_id" && t.Value == "req-acct")); } @@ -617,6 +628,7 @@ public void TrackDependency_IncludesAllClientContextTags() this.telemetryClient.Context.Device.Model = "Watch"; this.telemetryClient.Context.Device.OemName = "Apple"; this.telemetryClient.Context.Device.Type = "Wearable"; + this.telemetryClient.Context.Device.OperatingSystem = "watchOS 10"; var dep = new DependencyTelemetry { @@ -641,6 +653,7 @@ public void TrackDependency_IncludesAllClientContextTags() Assert.True(activity.Tags.Any(t => t.Key == "ai.device.model" && t.Value == "Watch")); Assert.True(activity.Tags.Any(t => t.Key == "ai.device.oemName" && t.Value == "Apple")); Assert.True(activity.Tags.Any(t => t.Key == "ai.device.type" && t.Value == "Wearable")); + Assert.True(activity.Tags.Any(t => t.Key == "ai.device.osVersion" && t.Value == "watchOS 10")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.synthetic_source" && t.Value == "dep-bot")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.user.account_id" && t.Value == "dep-acct")); } diff --git a/BASE/src/Microsoft.ApplicationInsights/Internal/SemanticConventions.cs b/BASE/src/Microsoft.ApplicationInsights/Internal/SemanticConventions.cs index 82160fc85d..dce1e8f327 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Internal/SemanticConventions.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Internal/SemanticConventions.cs @@ -250,6 +250,9 @@ internal static class SemanticConventions /// Attribute for device type. public const string AttributeAiDeviceType = "ai.device.type"; + /// Attribute for device OS version. + public const string AttributeAiDeviceOsVersion = "ai.device.osVersion"; + /// Attribute for synthetic source. public const string AttributeMicrosoftSyntheticSource = "microsoft.synthetic_source"; diff --git a/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs b/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs index 3ca37d2797..0518c9dab9 100644 --- a/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs +++ b/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs @@ -1491,6 +1491,11 @@ private static void ApplyContextToActivity(TelemetryContext context, Activity ac activity.SetTag(SemanticConventions.AttributeAiDeviceType, context.Device.Type); } + if (!string.IsNullOrEmpty(context.Device?.OperatingSystem)) + { + activity.SetTag(SemanticConventions.AttributeAiDeviceOsVersion, context.Device.OperatingSystem); + } + if (!string.IsNullOrEmpty(context.Operation?.SyntheticSource)) { activity.SetTag(SemanticConventions.AttributeMicrosoftSyntheticSource, context.Operation.SyntheticSource); @@ -1569,6 +1574,11 @@ private static void ApplyContextToProperties(TelemetryContext context, IDictiona properties[SemanticConventions.AttributeAiDeviceType] = context.Device.Type; } + if (!string.IsNullOrEmpty(context.Device?.OperatingSystem)) + { + properties[SemanticConventions.AttributeAiDeviceOsVersion] = context.Device.OperatingSystem; + } + if (!string.IsNullOrEmpty(context.Operation?.SyntheticSource)) { properties[SemanticConventions.AttributeMicrosoftSyntheticSource] = context.Operation.SyntheticSource; @@ -1662,6 +1672,11 @@ private Dictionary BuildContextTags() tags[SemanticConventions.AttributeAiDeviceType] = this.Context.Device.Type; } + if (!string.IsNullOrEmpty(this.Context.Device?.OperatingSystem)) + { + tags[SemanticConventions.AttributeAiDeviceOsVersion] = this.Context.Device.OperatingSystem; + } + if (!string.IsNullOrEmpty(this.Context.Operation?.SyntheticSource)) { tags[SemanticConventions.AttributeMicrosoftSyntheticSource] = this.Context.Operation.SyntheticSource; From f817df59a27642a71db3af830fd42618a368997a Mon Sep 17 00:00:00 2001 From: Harsimar Kaur Date: Thu, 5 Mar 2026 16:57:21 -0800 Subject: [PATCH 04/15] public cloud --- .../Stable/PublicAPI.Unshipped.txt | 6 ++++++ .../DataContracts/TelemetryContext.cs | 2 +- .../Extensibility/Implementation/CloudContext.cs | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt index 78547aeed0..acf7b451b4 100644 --- a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt @@ -24,3 +24,9 @@ Microsoft.ApplicationInsights.DataContracts.TelemetryContext.Component.get -> Mi Microsoft.ApplicationInsights.Extensibility.Implementation.ComponentContext Microsoft.ApplicationInsights.Extensibility.Implementation.ComponentContext.Version.get -> string Microsoft.ApplicationInsights.Extensibility.Implementation.ComponentContext.Version.set -> void +Microsoft.ApplicationInsights.DataContracts.TelemetryContext.Cloud.get -> Microsoft.ApplicationInsights.Extensibility.Implementation.CloudContext +Microsoft.ApplicationInsights.Extensibility.Implementation.CloudContext +Microsoft.ApplicationInsights.Extensibility.Implementation.CloudContext.RoleName.get -> string +Microsoft.ApplicationInsights.Extensibility.Implementation.CloudContext.RoleName.set -> void +Microsoft.ApplicationInsights.Extensibility.Implementation.CloudContext.RoleInstance.get -> string +Microsoft.ApplicationInsights.Extensibility.Implementation.CloudContext.RoleInstance.set -> void diff --git a/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs b/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs index 4f9c245840..efa779ad80 100644 --- a/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs @@ -105,7 +105,7 @@ public ComponentContext Component /// /// Gets the object describing the cloud tracked by this . /// - internal CloudContext Cloud + public CloudContext Cloud { get { return LazyInitializer.EnsureInitialized(ref this.cloud, () => new CloudContext()); } } diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/CloudContext.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/CloudContext.cs index b827919bda..49765997dd 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/CloudContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/CloudContext.cs @@ -5,7 +5,7 @@ /// /// Encapsulates information about a cloud where an application is running. /// - internal sealed class CloudContext + public sealed class CloudContext { /// /// Environment variable key used to communicate cloud role name override to the exporter. From 13b81bb9bc32b2f6d93d5c14119672dfb6c7f3aa Mon Sep 17 00:00:00 2001 From: Harsimar Kaur Date: Fri, 6 Mar 2026 16:57:03 -0800 Subject: [PATCH 05/15] remove context not in schema, remove metric context support, need to add processors and update docs --- .../Stable/PublicAPI.Unshipped.txt | 4 - BASE/README.md | 2 +- .../TelemetryClientContextTagsTest.cs | 306 +----------------- .../Implementation/DeviceContext.cs | 2 +- .../Implementation/SessionContext.cs | 2 +- .../Implementation/UserContext.cs | 2 +- .../Internal/SemanticConventions.cs | 6 - .../Microsoft.ApplicationInsights/Metric.cs | 238 +++++--------- .../Microsoft.ApplicationInsights.csproj | 2 +- .../TelemetryClient.cs | 116 +------ BreakingChanges.md | 5 +- MigrationGuidance.md | 1 - examples/BasicConsoleApp/Program.cs | 16 + 13 files changed, 103 insertions(+), 599 deletions(-) diff --git a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt index acf7b451b4..546aede6f2 100644 --- a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt @@ -5,8 +5,6 @@ Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.Id.get Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.Id.set -> void Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.Model.get -> string Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.Model.set -> void -Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.OemName.get -> string -Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.OemName.set -> void Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.OperatingSystem.get -> string Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.OperatingSystem.set -> void Microsoft.ApplicationInsights.Extensibility.Implementation.DeviceContext.Type.get -> string @@ -16,8 +14,6 @@ Microsoft.ApplicationInsights.Extensibility.Implementation.OperationContext.Synt Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext.Id.get -> string Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext.Id.set -> void -Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext.IsFirst.get -> bool? -Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext.IsFirst.set -> void Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.AccountId.get -> string Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.AccountId.set -> void Microsoft.ApplicationInsights.DataContracts.TelemetryContext.Component.get -> Microsoft.ApplicationInsights.Extensibility.Implementation.ComponentContext diff --git a/BASE/README.md b/BASE/README.md index 0e5dc20b0b..631cd8b5c2 100644 --- a/BASE/README.md +++ b/BASE/README.md @@ -425,7 +425,7 @@ In version 3.x, the following properties remain publicly settable on telemetry i **Available Context Properties:** | Context | Properties | Notes | |---------|-----------|-------| -| `User` | `Id`, `AuthenticatedUserId`, `UserAgent` | Be mindful of PII | +| `User` | `Id`, `AuthenticatedUserId` | Be mindful of PII | | `Operation` | `Name`| | | `Location` | `Ip` | | | `GlobalProperties` | (dictionary) | Custom key-value pairs | diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs index 1a227f74bf..c928413589 100644 --- a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs @@ -10,7 +10,6 @@ namespace Microsoft.ApplicationInsights using Microsoft.Extensions.Logging; using OpenTelemetry; using OpenTelemetry.Logs; - using OpenTelemetry.Metrics; using OpenTelemetry.Trace; using Xunit; @@ -23,7 +22,6 @@ namespace Microsoft.ApplicationInsights public class TelemetryClientContextTagsTest : IDisposable { private readonly List logItems; - private readonly List metricItems; private readonly List activityItems; private readonly TelemetryClient telemetryClient; @@ -32,13 +30,11 @@ public TelemetryClientContextTagsTest() var configuration = new TelemetryConfiguration(); configuration.SamplingRatio = 1.0f; this.logItems = new List(); - this.metricItems = new List(); this.activityItems = new List(); var instrumentationKey = Guid.NewGuid().ToString(); configuration.ConnectionString = "InstrumentationKey=" + instrumentationKey; configuration.ConfigureOpenTelemetryBuilder(b => b .WithLogging(l => l.AddInMemoryExporter(this.logItems)) - .WithMetrics(m => m.AddInMemoryExporter(this.metricItems)) .WithTracing(t => t.AddInMemoryExporter(this.activityItems))); this.telemetryClient = new TelemetryClient(configuration); } @@ -46,7 +42,6 @@ public TelemetryClientContextTagsTest() public void Dispose() { this.activityItems?.Clear(); - this.metricItems?.Clear(); this.logItems?.Clear(); this.telemetryClient?.TelemetryConfiguration?.Dispose(); } @@ -75,17 +70,6 @@ public void BuildContextTags_AuthenticatedUserIdMapsToEnduserId() Assert.Equal("auth-456", tags["enduser.id"]); } - [Fact] - public void BuildContextTags_UserAgentMapsToUserAgentOriginal() - { - this.telemetryClient.Context.User.UserAgent = "Mozilla/5.0"; - - var tags = this.telemetryClient.ContextTags; - - Assert.True(tags.ContainsKey("user_agent.original")); - Assert.Equal("Mozilla/5.0", tags["user_agent.original"]); - } - [Fact] public void BuildContextTags_OperationNameMapsToMicrosoftOperationName() { @@ -113,32 +97,26 @@ public void BuildContextTags_AllPropertiesSet_ContainsAllAttributes() { this.telemetryClient.Context.User.Id = "user-1"; this.telemetryClient.Context.User.AuthenticatedUserId = "auth-1"; - this.telemetryClient.Context.User.UserAgent = "TestAgent/1.0"; this.telemetryClient.Context.User.AccountId = "acct-1"; this.telemetryClient.Context.Operation.Name = "TestOp"; this.telemetryClient.Context.Operation.SyntheticSource = "bot"; this.telemetryClient.Context.Location.Ip = "192.168.1.1"; this.telemetryClient.Context.Session.Id = "session-1"; - this.telemetryClient.Context.Session.IsFirst = true; this.telemetryClient.Context.Device.Id = "device-1"; this.telemetryClient.Context.Device.Model = "Surface Pro"; - this.telemetryClient.Context.Device.OemName = "Microsoft"; this.telemetryClient.Context.Device.Type = "Tablet"; this.telemetryClient.Context.Device.OperatingSystem = "Windows 11"; var tags = this.telemetryClient.ContextTags; - Assert.Equal(14, tags.Count); + Assert.Equal(11, tags.Count); Assert.Equal("user-1", tags["enduser.pseudo.id"]); Assert.Equal("auth-1", tags["enduser.id"]); - Assert.Equal("TestAgent/1.0", tags["user_agent.original"]); Assert.Equal("TestOp", tags["microsoft.operation_name"]); Assert.Equal("192.168.1.1", tags["microsoft.client.ip"]); Assert.Equal("session-1", tags["microsoft.session.id"]); - Assert.Equal("True", tags["ai.session.isFirst"]); Assert.Equal("device-1", tags["ai.device.id"]); Assert.Equal("Surface Pro", tags["ai.device.model"]); - Assert.Equal("Microsoft", tags["ai.device.oemName"]); Assert.Equal("Tablet", tags["ai.device.type"]); Assert.Equal("Windows 11", tags["ai.device.osVersion"]); Assert.Equal("bot", tags["microsoft.synthetic_source"]); @@ -166,7 +144,6 @@ public void BuildContextTags_EmptyStringExcluded() { this.telemetryClient.Context.User.Id = string.Empty; this.telemetryClient.Context.User.AuthenticatedUserId = ""; - this.telemetryClient.Context.User.UserAgent = ""; this.telemetryClient.Context.User.AccountId = ""; this.telemetryClient.Context.Operation.Name = ""; this.telemetryClient.Context.Operation.SyntheticSource = ""; @@ -174,7 +151,6 @@ public void BuildContextTags_EmptyStringExcluded() this.telemetryClient.Context.Session.Id = ""; this.telemetryClient.Context.Device.Id = ""; this.telemetryClient.Context.Device.Model = ""; - this.telemetryClient.Context.Device.OemName = ""; this.telemetryClient.Context.Device.Type = ""; this.telemetryClient.Context.Device.OperatingSystem = ""; @@ -188,7 +164,6 @@ public void BuildContextTags_MixedSetAndUnset_OnlyIncludesNonEmpty() { this.telemetryClient.Context.User.Id = "user-1"; // AuthenticatedUserId not set (null) - this.telemetryClient.Context.User.UserAgent = ""; // empty this.telemetryClient.Context.Operation.Name = "MyOp"; // Location.Ip not set (null) @@ -242,16 +217,13 @@ public void TrackEvent_IncludesAllClientContextTags() { this.telemetryClient.Context.User.Id = "evt-user"; this.telemetryClient.Context.User.AuthenticatedUserId = "evt-auth"; - this.telemetryClient.Context.User.UserAgent = "EvtAgent/1.0"; this.telemetryClient.Context.User.AccountId = "evt-acct"; this.telemetryClient.Context.Operation.Name = "EvtOp"; this.telemetryClient.Context.Operation.SyntheticSource = "evt-bot"; this.telemetryClient.Context.Location.Ip = "10.0.0.2"; this.telemetryClient.Context.Session.Id = "evt-session"; - this.telemetryClient.Context.Session.IsFirst = true; this.telemetryClient.Context.Device.Id = "evt-device"; this.telemetryClient.Context.Device.Model = "Phone"; - this.telemetryClient.Context.Device.OemName = "Samsung"; this.telemetryClient.Context.Device.Type = "Mobile"; this.telemetryClient.Context.Device.OperatingSystem = "Android 14"; @@ -266,14 +238,11 @@ public void TrackEvent_IncludesAllClientContextTags() var attributes = logRecord.Attributes.ToDictionary(a => a.Key, a => a.Value?.ToString()); Assert.Equal("evt-user", attributes["enduser.pseudo.id"]); Assert.Equal("evt-auth", attributes["enduser.id"]); - Assert.Equal("EvtAgent/1.0", attributes["user_agent.original"]); Assert.Equal("EvtOp", attributes["microsoft.operation_name"]); Assert.Equal("10.0.0.2", attributes["microsoft.client.ip"]); Assert.Equal("evt-session", attributes["microsoft.session.id"]); - Assert.Equal("True", attributes["ai.session.isFirst"]); Assert.Equal("evt-device", attributes["ai.device.id"]); Assert.Equal("Phone", attributes["ai.device.model"]); - Assert.Equal("Samsung", attributes["ai.device.oemName"]); Assert.Equal("Mobile", attributes["ai.device.type"]); Assert.Equal("Android 14", attributes["ai.device.osVersion"]); Assert.Equal("evt-bot", attributes["microsoft.synthetic_source"]); @@ -314,16 +283,13 @@ public void TrackTrace_IncludesAllClientContextTags() { this.telemetryClient.Context.User.Id = "trc-user"; this.telemetryClient.Context.User.AuthenticatedUserId = "trc-auth"; - this.telemetryClient.Context.User.UserAgent = "TrcAgent/1.0"; this.telemetryClient.Context.User.AccountId = "trc-acct"; this.telemetryClient.Context.Operation.Name = "TrcOp"; this.telemetryClient.Context.Operation.SyntheticSource = "trc-bot"; this.telemetryClient.Context.Location.Ip = "10.0.0.3"; this.telemetryClient.Context.Session.Id = "trc-session"; - this.telemetryClient.Context.Session.IsFirst = false; this.telemetryClient.Context.Device.Id = "trc-device"; this.telemetryClient.Context.Device.Model = "Desktop"; - this.telemetryClient.Context.Device.OemName = "HP"; this.telemetryClient.Context.Device.Type = "PC"; this.telemetryClient.Context.Device.OperatingSystem = "Windows 10"; @@ -335,14 +301,11 @@ public void TrackTrace_IncludesAllClientContextTags() var attributes = logRecord.Attributes.ToDictionary(a => a.Key, a => a.Value?.ToString()); Assert.Equal("trc-user", attributes["enduser.pseudo.id"]); Assert.Equal("trc-auth", attributes["enduser.id"]); - Assert.Equal("TrcAgent/1.0", attributes["user_agent.original"]); Assert.Equal("TrcOp", attributes["microsoft.operation_name"]); Assert.Equal("10.0.0.3", attributes["microsoft.client.ip"]); Assert.Equal("trc-session", attributes["microsoft.session.id"]); - Assert.Equal("False", attributes["ai.session.isFirst"]); Assert.Equal("trc-device", attributes["ai.device.id"]); Assert.Equal("Desktop", attributes["ai.device.model"]); - Assert.Equal("HP", attributes["ai.device.oemName"]); Assert.Equal("PC", attributes["ai.device.type"]); Assert.Equal("Windows 10", attributes["ai.device.osVersion"]); Assert.Equal("trc-bot", attributes["microsoft.synthetic_source"]); @@ -354,16 +317,13 @@ public void TrackException_IncludesAllClientContextTags() { this.telemetryClient.Context.User.Id = "exc-user"; this.telemetryClient.Context.User.AuthenticatedUserId = "exc-auth"; - this.telemetryClient.Context.User.UserAgent = "ExcAgent/1.0"; this.telemetryClient.Context.User.AccountId = "exc-acct"; this.telemetryClient.Context.Operation.Name = "ExcOp"; this.telemetryClient.Context.Operation.SyntheticSource = "exc-bot"; this.telemetryClient.Context.Location.Ip = "172.16.0.1"; this.telemetryClient.Context.Session.Id = "exc-session"; - this.telemetryClient.Context.Session.IsFirst = true; this.telemetryClient.Context.Device.Id = "exc-device"; this.telemetryClient.Context.Device.Model = "Tablet"; - this.telemetryClient.Context.Device.OemName = "Asus"; this.telemetryClient.Context.Device.Type = "Portable"; this.telemetryClient.Context.Device.OperatingSystem = "Linux 6.1"; @@ -375,14 +335,11 @@ public void TrackException_IncludesAllClientContextTags() var attributes = logRecord.Attributes.ToDictionary(a => a.Key, a => a.Value?.ToString()); Assert.Equal("exc-user", attributes["enduser.pseudo.id"]); Assert.Equal("exc-auth", attributes["enduser.id"]); - Assert.Equal("ExcAgent/1.0", attributes["user_agent.original"]); Assert.Equal("ExcOp", attributes["microsoft.operation_name"]); Assert.Equal("172.16.0.1", attributes["microsoft.client.ip"]); Assert.Equal("exc-session", attributes["microsoft.session.id"]); - Assert.Equal("True", attributes["ai.session.isFirst"]); Assert.Equal("exc-device", attributes["ai.device.id"]); Assert.Equal("Tablet", attributes["ai.device.model"]); - Assert.Equal("Asus", attributes["ai.device.oemName"]); Assert.Equal("Portable", attributes["ai.device.type"]); Assert.Equal("Linux 6.1", attributes["ai.device.osVersion"]); Assert.Equal("exc-bot", attributes["microsoft.synthetic_source"]); @@ -584,10 +541,8 @@ public void TrackRequest_IncludesAllClientContextTags() this.telemetryClient.Context.User.AccountId = "req-acct"; this.telemetryClient.Context.Location.Ip = "10.0.0.1"; this.telemetryClient.Context.Session.Id = "req-session"; - this.telemetryClient.Context.Session.IsFirst = true; this.telemetryClient.Context.Device.Id = "req-device"; this.telemetryClient.Context.Device.Model = "Laptop"; - this.telemetryClient.Context.Device.OemName = "Dell"; this.telemetryClient.Context.Device.Type = "PC"; this.telemetryClient.Context.Device.OperatingSystem = "macOS 14"; this.telemetryClient.Context.Operation.SyntheticSource = "test-runner"; @@ -602,10 +557,8 @@ public void TrackRequest_IncludesAllClientContextTags() Assert.True(activity.Tags.Any(t => t.Key == "enduser.id" && t.Value == "req-auth")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.client.ip" && t.Value == "10.0.0.1")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.session.id" && t.Value == "req-session")); - Assert.True(activity.Tags.Any(t => t.Key == "ai.session.isFirst" && t.Value == "True")); Assert.True(activity.Tags.Any(t => t.Key == "ai.device.id" && t.Value == "req-device")); Assert.True(activity.Tags.Any(t => t.Key == "ai.device.model" && t.Value == "Laptop")); - Assert.True(activity.Tags.Any(t => t.Key == "ai.device.oemName" && t.Value == "Dell")); Assert.True(activity.Tags.Any(t => t.Key == "ai.device.type" && t.Value == "PC")); Assert.True(activity.Tags.Any(t => t.Key == "ai.device.osVersion" && t.Value == "macOS 14")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.synthetic_source" && t.Value == "test-runner")); @@ -617,16 +570,13 @@ public void TrackDependency_IncludesAllClientContextTags() { this.telemetryClient.Context.User.Id = "dep-user"; this.telemetryClient.Context.User.AuthenticatedUserId = "dep-auth"; - this.telemetryClient.Context.User.UserAgent = "DepAgent/1.0"; this.telemetryClient.Context.User.AccountId = "dep-acct"; this.telemetryClient.Context.Operation.Name = "ParentOp"; this.telemetryClient.Context.Operation.SyntheticSource = "dep-bot"; this.telemetryClient.Context.Location.Ip = "10.0.0.4"; this.telemetryClient.Context.Session.Id = "dep-session"; - this.telemetryClient.Context.Session.IsFirst = true; this.telemetryClient.Context.Device.Id = "dep-device"; this.telemetryClient.Context.Device.Model = "Watch"; - this.telemetryClient.Context.Device.OemName = "Apple"; this.telemetryClient.Context.Device.Type = "Wearable"; this.telemetryClient.Context.Device.OperatingSystem = "watchOS 10"; @@ -644,14 +594,11 @@ public void TrackDependency_IncludesAllClientContextTags() var activity = this.activityItems[0]; Assert.True(activity.Tags.Any(t => t.Key == "enduser.pseudo.id" && t.Value == "dep-user")); Assert.True(activity.Tags.Any(t => t.Key == "enduser.id" && t.Value == "dep-auth")); - Assert.True(activity.Tags.Any(t => t.Key == "user_agent.original" && t.Value == "DepAgent/1.0")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.operation_name" && t.Value == "ParentOp")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.client.ip" && t.Value == "10.0.0.4")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.session.id" && t.Value == "dep-session")); - Assert.True(activity.Tags.Any(t => t.Key == "ai.session.isFirst" && t.Value == "True")); Assert.True(activity.Tags.Any(t => t.Key == "ai.device.id" && t.Value == "dep-device")); Assert.True(activity.Tags.Any(t => t.Key == "ai.device.model" && t.Value == "Watch")); - Assert.True(activity.Tags.Any(t => t.Key == "ai.device.oemName" && t.Value == "Apple")); Assert.True(activity.Tags.Any(t => t.Key == "ai.device.type" && t.Value == "Wearable")); Assert.True(activity.Tags.Any(t => t.Key == "ai.device.osVersion" && t.Value == "watchOS 10")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.synthetic_source" && t.Value == "dep-bot")); @@ -766,209 +713,6 @@ public void TrackDependency_ItemOperationNameOverridesClientOperationName() #endregion - #region Metric Telemetry — Context Tags Applied - - [Fact] - public void TrackMetric_IncludesAllClientContextTags() - { - this.telemetryClient.Context.User.Id = "metric-user"; - this.telemetryClient.Context.User.AuthenticatedUserId = "metric-auth"; - this.telemetryClient.Context.User.UserAgent = "MetricAgent/1.0"; - this.telemetryClient.Context.User.AccountId = "metric-acct"; - this.telemetryClient.Context.Operation.Name = "MetricOp"; - this.telemetryClient.Context.Operation.SyntheticSource = "metric-bot"; - this.telemetryClient.Context.Location.Ip = "10.0.0.5"; - this.telemetryClient.Context.Session.Id = "metric-session"; - this.telemetryClient.Context.Session.IsFirst = true; - this.telemetryClient.Context.Device.Id = "metric-device"; - this.telemetryClient.Context.Device.Model = "Server"; - this.telemetryClient.Context.Device.OemName = "Lenovo"; - this.telemetryClient.Context.Device.Type = "Rack"; - - this.telemetryClient.TrackMetric("AllContextMetric", 42.0); - this.telemetryClient.Flush(); - - Assert.True(this.metricItems.Count > 0); - var metric = this.metricItems.FirstOrDefault(m => m.Name == "AllContextMetric"); - Assert.NotNull(metric); - - foreach (var point in metric.GetMetricPoints()) - { - var tagDict = new Dictionary(); - foreach (var tag in point.Tags) - { - tagDict[tag.Key] = tag.Value?.ToString(); - } - - Assert.Equal("metric-user", tagDict["enduser.pseudo.id"]); - Assert.Equal("metric-auth", tagDict["enduser.id"]); - Assert.Equal("MetricAgent/1.0", tagDict["user_agent.original"]); - Assert.Equal("MetricOp", tagDict["microsoft.operation_name"]); - Assert.Equal("10.0.0.5", tagDict["microsoft.client.ip"]); - Assert.Equal("metric-session", tagDict["microsoft.session.id"]); - Assert.Equal("True", tagDict["ai.session.isFirst"]); - Assert.Equal("metric-device", tagDict["ai.device.id"]); - Assert.Equal("Server", tagDict["ai.device.model"]); - Assert.Equal("Lenovo", tagDict["ai.device.oemName"]); - Assert.Equal("Rack", tagDict["ai.device.type"]); - Assert.Equal("metric-bot", tagDict["microsoft.synthetic_source"]); - Assert.Equal("metric-acct", tagDict["microsoft.user.account_id"]); - break; - } - } - - [Fact] - public void TrackMetric_PropertiesOverrideContextTags() - { - this.telemetryClient.Context.User.Id = "ctx-user"; - - this.telemetryClient.TrackMetric("TestMetric", 10.0, new Dictionary - { - { "enduser.pseudo.id", "prop-user" }, - { "custom", "value" }, - }); - this.telemetryClient.Flush(); - - var metric = this.metricItems.FirstOrDefault(m => m.Name == "TestMetric"); - Assert.NotNull(metric); - - foreach (var point in metric.GetMetricPoints()) - { - string userId = null; - string custom = null; - foreach (var tag in point.Tags) - { - if (tag.Key == "enduser.pseudo.id") - userId = tag.Value?.ToString(); - if (tag.Key == "custom") - custom = tag.Value?.ToString(); - } - // TagList appends in order; OTel should use the last value for duplicate keys - // Context tags are added first, then properties, so properties win - Assert.NotNull(userId); - Assert.Equal("prop-user", userId); - Assert.NotNull(custom); - Assert.Equal("value", custom); - break; - } - } - - [Fact] - public void GetMetric_TrackValue_IncludesAllClientContextTags() - { - this.telemetryClient.Context.User.Id = "getmetric-user"; - this.telemetryClient.Context.User.AuthenticatedUserId = "getmetric-auth"; - this.telemetryClient.Context.User.UserAgent = "GetMetricAgent/1.0"; - this.telemetryClient.Context.User.AccountId = "getmetric-acct"; - this.telemetryClient.Context.Operation.Name = "GetMetricOp"; - this.telemetryClient.Context.Operation.SyntheticSource = "getmetric-bot"; - this.telemetryClient.Context.Location.Ip = "10.0.0.6"; - this.telemetryClient.Context.Session.Id = "getmetric-session"; - this.telemetryClient.Context.Session.IsFirst = true; - this.telemetryClient.Context.Device.Id = "getmetric-device"; - this.telemetryClient.Context.Device.Model = "VM"; - this.telemetryClient.Context.Device.OemName = "Azure"; - this.telemetryClient.Context.Device.Type = "Cloud"; - - var metric = this.telemetryClient.GetMetric("AllContextGetMetric"); - metric.TrackValue(5.0); - - this.telemetryClient.Flush(); - - var collected = this.metricItems.FirstOrDefault(m => m.Name == "AllContextGetMetric"); - Assert.NotNull(collected); - - foreach (var point in collected.GetMetricPoints()) - { - var tagDict = new Dictionary(); - foreach (var tag in point.Tags) - { - tagDict[tag.Key] = tag.Value?.ToString(); - } - - Assert.Equal("getmetric-user", tagDict["enduser.pseudo.id"]); - Assert.Equal("getmetric-auth", tagDict["enduser.id"]); - Assert.Equal("GetMetricAgent/1.0", tagDict["user_agent.original"]); - Assert.Equal("GetMetricOp", tagDict["microsoft.operation_name"]); - Assert.Equal("10.0.0.6", tagDict["microsoft.client.ip"]); - Assert.Equal("getmetric-session", tagDict["microsoft.session.id"]); - Assert.Equal("True", tagDict["ai.session.isFirst"]); - Assert.Equal("getmetric-device", tagDict["ai.device.id"]); - Assert.Equal("VM", tagDict["ai.device.model"]); - Assert.Equal("Azure", tagDict["ai.device.oemName"]); - Assert.Equal("Cloud", tagDict["ai.device.type"]); - Assert.Equal("getmetric-bot", tagDict["microsoft.synthetic_source"]); - Assert.Equal("getmetric-acct", tagDict["microsoft.user.account_id"]); - break; - } - } - - [Fact] - public void GetMetric_TrackValue_OneDimension_IncludesContextTagsAndDimension() - { - this.telemetryClient.Context.User.Id = "dim-user"; - - var metric = this.telemetryClient.GetMetric("OneDimMetric", "region"); - metric.TrackValue(10.0, "us-west"); - - this.telemetryClient.Flush(); - - var collected = this.metricItems.FirstOrDefault(m => m.Name == "OneDimMetric"); - Assert.NotNull(collected); - - foreach (var point in collected.GetMetricPoints()) - { - bool hasUserId = false; - bool hasRegion = false; - foreach (var tag in point.Tags) - { - if (tag.Key == "enduser.pseudo.id" && tag.Value?.ToString() == "dim-user") - hasUserId = true; - if (tag.Key == "region" && tag.Value?.ToString() == "us-west") - hasRegion = true; - } - Assert.True(hasUserId, "Context tag should appear on metric"); - Assert.True(hasRegion, "Dimension should appear on metric"); - break; - } - } - - [Fact] - public void GetMetric_TrackValue_TwoDimensions_IncludesContextTagsAndDimensions() - { - this.telemetryClient.Context.Location.Ip = "192.168.1.1"; - - var metric = this.telemetryClient.GetMetric("TwoDimMetric", "region", "status"); - metric.TrackValue(7.0, "eu-west", "success"); - - this.telemetryClient.Flush(); - - var collected = this.metricItems.FirstOrDefault(m => m.Name == "TwoDimMetric"); - Assert.NotNull(collected); - - foreach (var point in collected.GetMetricPoints()) - { - bool hasIp = false; - bool hasRegion = false; - bool hasStatus = false; - foreach (var tag in point.Tags) - { - if (tag.Key == "microsoft.client.ip" && tag.Value?.ToString() == "192.168.1.1") - hasIp = true; - if (tag.Key == "region" && tag.Value?.ToString() == "eu-west") - hasRegion = true; - if (tag.Key == "status" && tag.Value?.ToString() == "success") - hasStatus = true; - } - Assert.True(hasIp, "Context tag should appear on metric"); - Assert.True(hasRegion, "Dimension 1 should appear on metric"); - Assert.True(hasStatus, "Dimension 2 should appear on metric"); - break; - } - } - - #endregion - #region Edge Cases [Fact] @@ -987,14 +731,11 @@ public void TrackEvent_NoContextSet_EmitsWithoutError() var attributes = logRecord.Attributes.ToDictionary(a => a.Key, a => a.Value?.ToString()); Assert.False(attributes.ContainsKey("enduser.pseudo.id")); Assert.False(attributes.ContainsKey("enduser.id")); - Assert.False(attributes.ContainsKey("user_agent.original")); Assert.False(attributes.ContainsKey("microsoft.operation_name")); Assert.False(attributes.ContainsKey("microsoft.client.ip")); Assert.False(attributes.ContainsKey("microsoft.session.id")); - Assert.False(attributes.ContainsKey("ai.session.isFirst")); Assert.False(attributes.ContainsKey("ai.device.id")); Assert.False(attributes.ContainsKey("ai.device.model")); - Assert.False(attributes.ContainsKey("ai.device.oemName")); Assert.False(attributes.ContainsKey("ai.device.type")); Assert.False(attributes.ContainsKey("microsoft.synthetic_source")); Assert.False(attributes.ContainsKey("microsoft.user.account_id")); @@ -1013,30 +754,6 @@ public void TrackTrace_NullProperties_ContextTagsStillApplied() Assert.Equal("null-props-user", attributes["enduser.pseudo.id"]); } - [Fact] - public void TrackMetric_NullProperties_ContextTagsStillApplied() - { - this.telemetryClient.Context.User.Id = "null-metric-user"; - - this.telemetryClient.TrackMetric("NullPropMetric", 1.0, null); - this.telemetryClient.Flush(); - - var metric = this.metricItems.FirstOrDefault(m => m.Name == "NullPropMetric"); - Assert.NotNull(metric); - - foreach (var point in metric.GetMetricPoints()) - { - bool hasUserId = false; - foreach (var tag in point.Tags) - { - if (tag.Key == "enduser.pseudo.id" && tag.Value?.ToString() == "null-metric-user") - hasUserId = true; - } - Assert.True(hasUserId, "Context tag should appear even with null properties"); - break; - } - } - [Fact] public void TrackRequest_NoContextSet_EmitsWithoutContextTags() { @@ -1049,36 +766,15 @@ public void TrackRequest_NoContextSet_EmitsWithoutContextTags() Assert.False(activity.Tags.Any(t => t.Key == "enduser.pseudo.id")); Assert.False(activity.Tags.Any(t => t.Key == "enduser.id")); - Assert.False(activity.Tags.Any(t => t.Key == "user_agent.original")); Assert.False(activity.Tags.Any(t => t.Key == "microsoft.client.ip")); Assert.False(activity.Tags.Any(t => t.Key == "microsoft.session.id")); - Assert.False(activity.Tags.Any(t => t.Key == "ai.session.isFirst")); Assert.False(activity.Tags.Any(t => t.Key == "ai.device.id")); Assert.False(activity.Tags.Any(t => t.Key == "ai.device.model")); - Assert.False(activity.Tags.Any(t => t.Key == "ai.device.oemName")); Assert.False(activity.Tags.Any(t => t.Key == "ai.device.type")); Assert.False(activity.Tags.Any(t => t.Key == "microsoft.synthetic_source")); Assert.False(activity.Tags.Any(t => t.Key == "microsoft.user.account_id")); } - [Fact] - public void GetMetric_NoContextSet_TrackValueSucceeds() - { - // No context set - var metric = this.telemetryClient.GetMetric("EmptyCtxMetric"); - metric.TrackValue(3.0); - this.telemetryClient.Flush(); - - var collected = this.metricItems.FirstOrDefault(m => m.Name == "EmptyCtxMetric"); - Assert.NotNull(collected); - - foreach (var point in collected.GetMetricPoints()) - { - Assert.True(point.GetHistogramCount() > 0); - break; - } - } - #endregion } } diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/DeviceContext.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/DeviceContext.cs index fe50903339..f74dc73739 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/DeviceContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/DeviceContext.cs @@ -50,7 +50,7 @@ public string OperatingSystem /// /// Gets or sets the device OEM for the current device. /// - public string OemName + internal string OemName { get { return string.IsNullOrEmpty(this.oemName) ? null : this.oemName; } set { this.oemName = value; } diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/SessionContext.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/SessionContext.cs index 49d82c1946..c1c36b6f69 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/SessionContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/SessionContext.cs @@ -24,7 +24,7 @@ public string Id /// /// Gets or sets the IsFirst Session for the user. /// - public bool? IsFirst + internal bool? IsFirst { get { return this.isFirst; } set { this.isFirst = value; } diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs index 7eae54e7ea..f829c4c147 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs @@ -29,7 +29,7 @@ public string Id /// /// Gets or sets the UserAgent of an application-defined account associated with the user. /// - public string UserAgent + internal string UserAgent { get { return string.IsNullOrEmpty(this.userAgent) ? null : this.userAgent; } set { this.userAgent = value; } diff --git a/BASE/src/Microsoft.ApplicationInsights/Internal/SemanticConventions.cs b/BASE/src/Microsoft.ApplicationInsights/Internal/SemanticConventions.cs index dce1e8f327..0c57e6b08f 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Internal/SemanticConventions.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Internal/SemanticConventions.cs @@ -235,18 +235,12 @@ internal static class SemanticConventions /// Attribute for session ID. public const string AttributeMicrosoftSessionId = "microsoft.session.id"; - /// Attribute for first session flag. - public const string AttributeAiSessionIsFirst = "ai.session.isFirst"; - /// Attribute for device ID. public const string AttributeAiDeviceId = "ai.device.id"; /// Attribute for device model. public const string AttributeAiDeviceModel = "ai.device.model"; - /// Attribute for device OEM name. - public const string AttributeAiDeviceOemName = "ai.device.oemName"; - /// Attribute for device type. public const string AttributeAiDeviceType = "ai.device.type"; diff --git a/BASE/src/Microsoft.ApplicationInsights/Metric.cs b/BASE/src/Microsoft.ApplicationInsights/Metric.cs index 5250bc529e..99d6dc7c72 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Metric.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Metric.cs @@ -47,22 +47,7 @@ public void TrackValue(double metricValue) this.metricName, this.metricNamespace); - var allTags = this.client.BuildBaseMetricTags(); - - if (allTags.Count > 0) - { - var tags = new TagList(); - foreach (var kvp in allTags) - { - tags.Add(kvp.Key, kvp.Value); - } - - histogram.Record(metricValue, tags); - } - else - { histogram.Record(metricValue); - } } /// @@ -107,19 +92,7 @@ public bool TrackValue(double metricValue, string dimension1Value) this.metricName, this.metricNamespace); - var allTags = this.client.BuildBaseMetricTags(); - - // Dimension values (highest priority) - allTags[this.dimensionNames[0]] = dimension1Value; - - // Convert to TagList for OTel histogram recording - var tags = new TagList(); - foreach (var kvp in allTags) - { - tags.Add(kvp.Key, kvp.Value); - } - - histogram.Record(metricValue, tags); + histogram.Record(metricValue, new TagList { { this.dimensionNames[0], dimension1Value } }); return true; } @@ -172,18 +145,11 @@ public bool TrackValue(double metricValue, string dimension1Value, string dimens this.metricName, this.metricNamespace); - var allTags = this.client.BuildBaseMetricTags(); - - // Dimension values (highest priority) - allTags[this.dimensionNames[0]] = dimension1Value; - allTags[this.dimensionNames[1]] = dimension2Value; - - // Convert to TagList for OTel histogram recording - var tags = new TagList(); - foreach (var kvp in allTags) + var tags = new TagList { - tags.Add(kvp.Key, kvp.Value); - } + { this.dimensionNames[0], dimension1Value }, + { this.dimensionNames[1], dimension2Value }, + }; histogram.Record(metricValue, tags); return true; @@ -240,19 +206,12 @@ public bool TrackValue(double metricValue, string dimension1Value, string dimens this.metricName, this.metricNamespace); - var allTags = this.client.BuildBaseMetricTags(); - - // Dimension values (highest priority) - allTags[this.dimensionNames[0]] = dimension1Value; - allTags[this.dimensionNames[1]] = dimension2Value; - allTags[this.dimensionNames[2]] = dimension3Value; - - // Convert to TagList for OTel histogram recording - var tags = new TagList(); - foreach (var kvp in allTags) + var tags = new TagList { - tags.Add(kvp.Key, kvp.Value); - } + { this.dimensionNames[0], dimension1Value }, + { this.dimensionNames[1], dimension2Value }, + { this.dimensionNames[2], dimension3Value }, + }; histogram.Record(metricValue, tags); return true; @@ -311,20 +270,13 @@ public bool TrackValue(double metricValue, string dimension1Value, string dimens this.metricName, this.metricNamespace); - var allTags = this.client.BuildBaseMetricTags(); - - // Dimension values (highest priority) - allTags[this.dimensionNames[0]] = dimension1Value; - allTags[this.dimensionNames[1]] = dimension2Value; - allTags[this.dimensionNames[2]] = dimension3Value; - allTags[this.dimensionNames[3]] = dimension4Value; - - // Convert to TagList for OTel histogram recording - var tags = new TagList(); - foreach (var kvp in allTags) + var tags = new TagList { - tags.Add(kvp.Key, kvp.Value); - } + { this.dimensionNames[0], dimension1Value }, + { this.dimensionNames[1], dimension2Value }, + { this.dimensionNames[2], dimension3Value }, + { this.dimensionNames[3], dimension4Value }, + }; histogram.Record(metricValue, tags); return true; @@ -391,21 +343,14 @@ public bool TrackValue( this.metricName, this.metricNamespace); - var allTags = this.client.BuildBaseMetricTags(); - - // Dimension values (highest priority) - allTags[this.dimensionNames[0]] = dimension1Value; - allTags[this.dimensionNames[1]] = dimension2Value; - allTags[this.dimensionNames[2]] = dimension3Value; - allTags[this.dimensionNames[3]] = dimension4Value; - allTags[this.dimensionNames[4]] = dimension5Value; - - // Convert to TagList for OTel histogram recording - var tags = new TagList(); - foreach (var kvp in allTags) + var tags = new TagList { - tags.Add(kvp.Key, kvp.Value); - } + { this.dimensionNames[0], dimension1Value }, + { this.dimensionNames[1], dimension2Value }, + { this.dimensionNames[2], dimension3Value }, + { this.dimensionNames[3], dimension4Value }, + { this.dimensionNames[4], dimension5Value }, + }; histogram.Record(metricValue, tags); return true; @@ -481,22 +426,15 @@ public bool TrackValue( this.metricName, this.metricNamespace); - var allTags = this.client.BuildBaseMetricTags(); - - // Dimension values (highest priority) - allTags[this.dimensionNames[0]] = dimension1Value; - allTags[this.dimensionNames[1]] = dimension2Value; - allTags[this.dimensionNames[2]] = dimension3Value; - allTags[this.dimensionNames[3]] = dimension4Value; - allTags[this.dimensionNames[4]] = dimension5Value; - allTags[this.dimensionNames[5]] = dimension6Value; - - // Convert to TagList for OTel histogram recording - var tags = new TagList(); - foreach (var kvp in allTags) + var tags = new TagList { - tags.Add(kvp.Key, kvp.Value); - } + { this.dimensionNames[0], dimension1Value }, + { this.dimensionNames[1], dimension2Value }, + { this.dimensionNames[2], dimension3Value }, + { this.dimensionNames[3], dimension4Value }, + { this.dimensionNames[4], dimension5Value }, + { this.dimensionNames[5], dimension6Value }, + }; histogram.Record(metricValue, tags); return true; @@ -576,23 +514,16 @@ public bool TrackValue( this.metricName, this.metricNamespace); - var allTags = this.client.BuildBaseMetricTags(); - - // Dimension values (highest priority) - allTags[this.dimensionNames[0]] = dimension1Value; - allTags[this.dimensionNames[1]] = dimension2Value; - allTags[this.dimensionNames[2]] = dimension3Value; - allTags[this.dimensionNames[3]] = dimension4Value; - allTags[this.dimensionNames[4]] = dimension5Value; - allTags[this.dimensionNames[5]] = dimension6Value; - allTags[this.dimensionNames[6]] = dimension7Value; - - // Convert to TagList for OTel histogram recording - var tags = new TagList(); - foreach (var kvp in allTags) + var tags = new TagList { - tags.Add(kvp.Key, kvp.Value); - } + { this.dimensionNames[0], dimension1Value }, + { this.dimensionNames[1], dimension2Value }, + { this.dimensionNames[2], dimension3Value }, + { this.dimensionNames[3], dimension4Value }, + { this.dimensionNames[4], dimension5Value }, + { this.dimensionNames[5], dimension6Value }, + { this.dimensionNames[6], dimension7Value }, + }; histogram.Record(metricValue, tags); return true; @@ -676,24 +607,17 @@ public bool TrackValue( this.metricName, this.metricNamespace); - var allTags = this.client.BuildBaseMetricTags(); - - // Dimension values (highest priority) - allTags[this.dimensionNames[0]] = dimension1Value; - allTags[this.dimensionNames[1]] = dimension2Value; - allTags[this.dimensionNames[2]] = dimension3Value; - allTags[this.dimensionNames[3]] = dimension4Value; - allTags[this.dimensionNames[4]] = dimension5Value; - allTags[this.dimensionNames[5]] = dimension6Value; - allTags[this.dimensionNames[6]] = dimension7Value; - allTags[this.dimensionNames[7]] = dimension8Value; - - // Convert to TagList for OTel histogram recording - var tags = new TagList(); - foreach (var kvp in allTags) + var tags = new TagList { - tags.Add(kvp.Key, kvp.Value); - } + { this.dimensionNames[0], dimension1Value }, + { this.dimensionNames[1], dimension2Value }, + { this.dimensionNames[2], dimension3Value }, + { this.dimensionNames[3], dimension4Value }, + { this.dimensionNames[4], dimension5Value }, + { this.dimensionNames[5], dimension6Value }, + { this.dimensionNames[6], dimension7Value }, + { this.dimensionNames[7], dimension8Value }, + }; histogram.Record(metricValue, tags); return true; @@ -781,25 +705,18 @@ public bool TrackValue( this.metricName, this.metricNamespace); - var allTags = this.client.BuildBaseMetricTags(); - - // Dimension values (highest priority) - allTags[this.dimensionNames[0]] = dimension1Value; - allTags[this.dimensionNames[1]] = dimension2Value; - allTags[this.dimensionNames[2]] = dimension3Value; - allTags[this.dimensionNames[3]] = dimension4Value; - allTags[this.dimensionNames[4]] = dimension5Value; - allTags[this.dimensionNames[5]] = dimension6Value; - allTags[this.dimensionNames[6]] = dimension7Value; - allTags[this.dimensionNames[7]] = dimension8Value; - allTags[this.dimensionNames[8]] = dimension9Value; - - // Convert to TagList for OTel histogram recording - var tags = new TagList(); - foreach (var kvp in allTags) + var tags = new TagList { - tags.Add(kvp.Key, kvp.Value); - } + { this.dimensionNames[0], dimension1Value }, + { this.dimensionNames[1], dimension2Value }, + { this.dimensionNames[2], dimension3Value }, + { this.dimensionNames[3], dimension4Value }, + { this.dimensionNames[4], dimension5Value }, + { this.dimensionNames[5], dimension6Value }, + { this.dimensionNames[6], dimension7Value }, + { this.dimensionNames[7], dimension8Value }, + { this.dimensionNames[8], dimension9Value }, + }; histogram.Record(metricValue, tags); return true; @@ -891,26 +808,19 @@ public bool TrackValue( this.metricName, this.metricNamespace); - var allTags = this.client.BuildBaseMetricTags(); - - // Dimension values (highest priority) - allTags[this.dimensionNames[0]] = dimension1Value; - allTags[this.dimensionNames[1]] = dimension2Value; - allTags[this.dimensionNames[2]] = dimension3Value; - allTags[this.dimensionNames[3]] = dimension4Value; - allTags[this.dimensionNames[4]] = dimension5Value; - allTags[this.dimensionNames[5]] = dimension6Value; - allTags[this.dimensionNames[6]] = dimension7Value; - allTags[this.dimensionNames[7]] = dimension8Value; - allTags[this.dimensionNames[8]] = dimension9Value; - allTags[this.dimensionNames[9]] = dimension10Value; - - // Convert to TagList for OTel histogram recording - var tags = new TagList(); - foreach (var kvp in allTags) - { - tags.Add(kvp.Key, kvp.Value); - } + var tags = new TagList + { + { this.dimensionNames[0], dimension1Value }, + { this.dimensionNames[1], dimension2Value }, + { this.dimensionNames[2], dimension3Value }, + { this.dimensionNames[3], dimension4Value }, + { this.dimensionNames[4], dimension5Value }, + { this.dimensionNames[5], dimension6Value }, + { this.dimensionNames[6], dimension7Value }, + { this.dimensionNames[7], dimension8Value }, + { this.dimensionNames[8], dimension9Value }, + { this.dimensionNames[9], dimension10Value }, + }; histogram.Record(metricValue, tags); return true; diff --git a/BASE/src/Microsoft.ApplicationInsights/Microsoft.ApplicationInsights.csproj b/BASE/src/Microsoft.ApplicationInsights/Microsoft.ApplicationInsights.csproj index 6649471f86..62a9079eb3 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Microsoft.ApplicationInsights.csproj +++ b/BASE/src/Microsoft.ApplicationInsights/Microsoft.ApplicationInsights.csproj @@ -27,7 +27,7 @@ - + All diff --git a/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs b/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs index 0518c9dab9..4fa128979a 100644 --- a/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs +++ b/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs @@ -380,22 +380,10 @@ public void TrackMetric(string name, double value, IDictionary p // Get or create histogram for this metric var histogram = this.Configuration.MetricsManager.GetOrCreateHistogram(name, null); - // Merge: client context tags + GlobalProperties (lowest) → caller properties (highest) - var allTags = this.BuildBaseMetricTags(); - - // Caller-provided properties (highest priority) - if (properties != null) - { - foreach (var kvp in properties) - { - allTags[kvp.Key] = kvp.Value; - } - } - - if (allTags.Count > 0) + if (properties != null && properties.Count > 0) { var tags = new TagList(); - foreach (var kvp in allTags) + foreach (var kvp in properties) { tags.Add(kvp.Key, kvp.Value); } @@ -431,34 +419,10 @@ public void TrackMetric(MetricTelemetry telemetry) telemetry.Name, telemetry.MetricNamespace); - // Merge: client context tags + GlobalProperties (lowest) → item layers (highest) - var allTags = this.BuildBaseMetricTags(); - - // 3. GlobalProperties from item context - if (telemetry.Context?.GlobalPropertiesValue != null) - { - foreach (var property in telemetry.Context.GlobalPropertiesValue) - { - allTags[property.Key] = property.Value; - } - } - - // 4. Item custom properties - if (telemetry.Properties != null) - { - foreach (var kvp in telemetry.Properties) - { - allTags[kvp.Key] = kvp.Value; - } - } - - // 5. Item-level context (highest priority) - ApplyContextToProperties(telemetry.Context, allTags); - - if (allTags.Count > 0) + if (telemetry.Properties != null && telemetry.Properties.Count > 0) { var tags = new TagList(); - foreach (var kvp in allTags) + foreach (var kvp in telemetry.Properties) { tags.Add(kvp.Key, kvp.Value); } @@ -1156,32 +1120,7 @@ public Metric GetMetric( return new Metric(this, metricIdentifier.MetricId, metricIdentifier.MetricNamespace, dimensionNames); } - /// - /// Builds a dictionary of metric tags by merging client-level context tags and GlobalProperties. - /// Context tags have lowest priority; GlobalProperties override them. - /// Used by both and to avoid duplicating merge logic. - /// - internal Dictionary BuildBaseMetricTags() - { - var allTags = new Dictionary(); - - // 1. Client-level context tags (lowest priority) - foreach (var tag in this.ContextTags) - { - allTags[tag.Key] = tag.Value; - } - - // 2. GlobalProperties from client context - if (this.Context.GlobalPropertiesValue != null) - { - foreach (var property in this.Context.GlobalPropertiesValue) - { - allTags[property.Key] = property.Value; - } - } - - return allTags; - } + private static LogLevel GetLogLevel(SeverityLevel severityLevel) { @@ -1446,11 +1385,6 @@ private static void ApplyContextToActivity(TelemetryContext context, Activity ac activity.SetTag(SemanticConventions.AttributeEnduserId, context.User.AuthenticatedUserId); } - if (!string.IsNullOrEmpty(context.User?.UserAgent)) - { - activity.SetTag(SemanticConventions.AttributeUserAgentOriginal, context.User.UserAgent); - } - if (!string.IsNullOrEmpty(context.Operation?.Name)) { activity.SetTag(SemanticConventions.AttributeMicrosoftOperationName, context.Operation.Name); @@ -1466,11 +1400,6 @@ private static void ApplyContextToActivity(TelemetryContext context, Activity ac activity.SetTag(SemanticConventions.AttributeMicrosoftSessionId, context.Session.Id); } - if (context.Session?.IsFirst != null) - { - activity.SetTag(SemanticConventions.AttributeAiSessionIsFirst, context.Session.IsFirst.Value.ToString()); - } - if (!string.IsNullOrEmpty(context.Device?.Id)) { activity.SetTag(SemanticConventions.AttributeAiDeviceId, context.Device.Id); @@ -1481,11 +1410,6 @@ private static void ApplyContextToActivity(TelemetryContext context, Activity ac activity.SetTag(SemanticConventions.AttributeAiDeviceModel, context.Device.Model); } - if (!string.IsNullOrEmpty(context.Device?.OemName)) - { - activity.SetTag(SemanticConventions.AttributeAiDeviceOemName, context.Device.OemName); - } - if (!string.IsNullOrEmpty(context.Device?.Type)) { activity.SetTag(SemanticConventions.AttributeAiDeviceType, context.Device.Type); @@ -1529,11 +1453,6 @@ private static void ApplyContextToProperties(TelemetryContext context, IDictiona properties[SemanticConventions.AttributeEnduserId] = context.User.AuthenticatedUserId; } - if (!string.IsNullOrEmpty(context.User?.UserAgent)) - { - properties[SemanticConventions.AttributeUserAgentOriginal] = context.User.UserAgent; - } - if (!string.IsNullOrEmpty(context.Operation?.Name)) { properties[SemanticConventions.AttributeMicrosoftOperationName] = context.Operation.Name; @@ -1549,11 +1468,6 @@ private static void ApplyContextToProperties(TelemetryContext context, IDictiona properties[SemanticConventions.AttributeMicrosoftSessionId] = context.Session.Id; } - if (context.Session?.IsFirst != null) - { - properties[SemanticConventions.AttributeAiSessionIsFirst] = context.Session.IsFirst.Value.ToString(); - } - if (!string.IsNullOrEmpty(context.Device?.Id)) { properties[SemanticConventions.AttributeAiDeviceId] = context.Device.Id; @@ -1564,11 +1478,6 @@ private static void ApplyContextToProperties(TelemetryContext context, IDictiona properties[SemanticConventions.AttributeAiDeviceModel] = context.Device.Model; } - if (!string.IsNullOrEmpty(context.Device?.OemName)) - { - properties[SemanticConventions.AttributeAiDeviceOemName] = context.Device.OemName; - } - if (!string.IsNullOrEmpty(context.Device?.Type)) { properties[SemanticConventions.AttributeAiDeviceType] = context.Device.Type; @@ -1627,11 +1536,6 @@ private Dictionary BuildContextTags() tags[SemanticConventions.AttributeEnduserId] = this.Context.User.AuthenticatedUserId; } - if (!string.IsNullOrEmpty(this.Context.User?.UserAgent)) - { - tags[SemanticConventions.AttributeUserAgentOriginal] = this.Context.User.UserAgent; - } - if (!string.IsNullOrEmpty(this.Context.Operation?.Name)) { tags[SemanticConventions.AttributeMicrosoftOperationName] = this.Context.Operation.Name; @@ -1647,11 +1551,6 @@ private Dictionary BuildContextTags() tags[SemanticConventions.AttributeMicrosoftSessionId] = this.Context.Session.Id; } - if (this.Context.Session?.IsFirst != null) - { - tags[SemanticConventions.AttributeAiSessionIsFirst] = this.Context.Session.IsFirst.Value.ToString(); - } - if (!string.IsNullOrEmpty(this.Context.Device?.Id)) { tags[SemanticConventions.AttributeAiDeviceId] = this.Context.Device.Id; @@ -1662,11 +1561,6 @@ private Dictionary BuildContextTags() tags[SemanticConventions.AttributeAiDeviceModel] = this.Context.Device.Model; } - if (!string.IsNullOrEmpty(this.Context.Device?.OemName)) - { - tags[SemanticConventions.AttributeAiDeviceOemName] = this.Context.Device.OemName; - } - if (!string.IsNullOrEmpty(this.Context.Device?.Type)) { tags[SemanticConventions.AttributeAiDeviceType] = this.Context.Device.Type; diff --git a/BreakingChanges.md b/BreakingChanges.md index fa21b85ec8..61013e8204 100644 --- a/BreakingChanges.md +++ b/BreakingChanges.md @@ -153,6 +153,7 @@ See detailed migration guidance [here](MigrationGuidance.md#telemetry-context) ### Sub-Context Properties Made Internal The following properties on **still-public** sub-context classes have been made internal: +- **`User.UserAgent`** — Was public in 2.x, now internal. The Application Insights ingestion service does not surface this field in workspace-based resources. - **`User.AccountId`** — Was public in 2.x, now internal. This can be set via adding properties to Track() calls or creating custom OpenTelemetry processors. - **`Operation.Id`** — Was public in 2.x, now internal. Correlation IDs are managed automatically by OpenTelemetry. - **`Operation.ParentId`** — Was public in 2.x, now internal. Correlation IDs are managed automatically by OpenTelemetry. @@ -161,12 +162,10 @@ The following properties on **still-public** sub-context classes have been made ### Properties Retained The following remain **public**: -- `User` (`Id`, `AuthenticatedUserId`, `UserAgent`) +- `User` (`Id`, `AuthenticatedUserId`) - `Operation` (`Name`) - `Location` (`Ip`) - `GlobalProperties` - -Note that these properties are currently settable on individual telemetry items; there is future work planned to make these settable via TelemetryClient. --- # 2. Microsoft.ApplicationInsights.AspNetCore diff --git a/MigrationGuidance.md b/MigrationGuidance.md index ca1251fedc..6d3ce90346 100644 --- a/MigrationGuidance.md +++ b/MigrationGuidance.md @@ -394,7 +394,6 @@ client.Context.GlobalProperties["MyCustomGlobalProperty"] = "Production"; // Once fixed, setting them here will apply to all telemetry sent by this client: client.Context.User.Id = "anonymous-user-id"; client.Context.User.AuthenticatedUserId = "authenticated-user-id"; -client.Context.User.UserAgent = "MyApp/1.0"; client.Context.Operation.Name = "MyOperation"; client.Context.Location.Ip = "127.0.0.1"; ``` diff --git a/examples/BasicConsoleApp/Program.cs b/examples/BasicConsoleApp/Program.cs index ab3e9a0e58..eb92e86943 100644 --- a/examples/BasicConsoleApp/Program.cs +++ b/examples/BasicConsoleApp/Program.cs @@ -28,6 +28,22 @@ static void Main(string[] args) // Initialize the TelemetryClient var telemetryClient = new TelemetryClient(telemetryConfig); + // Set all public TelemetryContext properties to verify context tag propagation + telemetryClient.Context.User.Id = "test-user-123"; + telemetryClient.Context.User.AuthenticatedUserId = "auth-user-456"; + telemetryClient.Context.User.AccountId = "account-789"; + telemetryClient.Context.Session.Id = "session-abc"; + telemetryClient.Context.Device.Id = "device-xyz"; + telemetryClient.Context.Device.Model = "Surface Pro"; + telemetryClient.Context.Device.Type = "PC"; + telemetryClient.Context.Device.OperatingSystem = "Windows 11"; + telemetryClient.Context.Operation.Name = "TestOperation"; + telemetryClient.Context.Operation.SyntheticSource = "test-bot"; + telemetryClient.Context.Location.Ip = "10.0.0.1"; + telemetryClient.Context.Cloud.RoleName = "TestRole"; + telemetryClient.Context.Cloud.RoleInstance = "TestInstance"; + telemetryClient.Context.Component.Version = "2.0.0"; + // **The following lines are examples of tracking different telemetry types.** telemetryClient.TrackEvent("SampleEvent"); From 38e697b5ce5a026fda1d535cb803604f06cb6698 Mon Sep 17 00:00:00 2001 From: Harsimar Kaur Date: Mon, 9 Mar 2026 17:07:06 -0700 Subject: [PATCH 06/15] p1 - add processors, will use later --- .../Stable/PublicAPI.Shipped.txt | 2 - .../Implementation/DeviceContext.cs | 16 +-- .../Implementation/UserContext.cs | 18 +-- .../Microsoft.ApplicationInsights/Metric.cs | 2 +- .../TelemetryContextActivityProcessor.cs | 64 +++++++++++ .../TelemetryContextLogProcessor.cs | 103 ++++++++++++++++++ .../TelemetryClient.cs | 2 - 7 files changed, 185 insertions(+), 22 deletions(-) create mode 100644 BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs create mode 100644 BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs diff --git a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Shipped.txt b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Shipped.txt index 9853129d93..8e7e01dff8 100644 --- a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Shipped.txt +++ b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Shipped.txt @@ -146,8 +146,6 @@ Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.Authentic Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.AuthenticatedUserId.set -> void Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.Id.get -> string Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.Id.set -> void -Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.UserAgent.get -> string -Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.UserAgent.set -> void Microsoft.ApplicationInsights.Extensibility.IOperationHolder Microsoft.ApplicationInsights.Extensibility.IOperationHolder.Telemetry.get -> T Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/DeviceContext.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/DeviceContext.cs index f74dc73739..9af740577d 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/DeviceContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/DeviceContext.cs @@ -48,21 +48,21 @@ public string OperatingSystem } /// - /// Gets or sets the device OEM for the current device. + /// Gets or sets the device model for the current device. /// - internal string OemName + public string Model { - get { return string.IsNullOrEmpty(this.oemName) ? null : this.oemName; } - set { this.oemName = value; } + get { return string.IsNullOrEmpty(this.model) ? null : this.model; } + set { this.model = value; } } /// - /// Gets or sets the device model for the current device. + /// Gets or sets the device OEM for the current device. /// - public string Model + internal string OemName { - get { return string.IsNullOrEmpty(this.model) ? null : this.model; } - set { this.model = value; } + get { return string.IsNullOrEmpty(this.oemName) ? null : this.oemName; } + set { this.oemName = value; } } } } diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs index f829c4c147..bce14d4362 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs @@ -26,15 +26,6 @@ public string Id set { this.id = value; } } - /// - /// Gets or sets the UserAgent of an application-defined account associated with the user. - /// - internal string UserAgent - { - get { return string.IsNullOrEmpty(this.userAgent) ? null : this.userAgent; } - set { this.userAgent = value; } - } - /// /// Gets or sets the authenticated user id. /// Authenticated user id should be a persistent string that uniquely represents each authenticated user in the application or service. @@ -53,5 +44,14 @@ public string AccountId get { return string.IsNullOrEmpty(this.accountId) ? null : this.accountId; } set { this.accountId = value; } } + + /// + /// Gets or sets the UserAgent of an application-defined account associated with the user. + /// + internal string UserAgent + { + get { return string.IsNullOrEmpty(this.userAgent) ? null : this.userAgent; } + set { this.userAgent = value; } + } } } diff --git a/BASE/src/Microsoft.ApplicationInsights/Metric.cs b/BASE/src/Microsoft.ApplicationInsights/Metric.cs index 99d6dc7c72..49aa9fe97f 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Metric.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Metric.cs @@ -47,7 +47,7 @@ public void TrackValue(double metricValue) this.metricName, this.metricNamespace); - histogram.Record(metricValue); + histogram.Record(metricValue); } /// diff --git a/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs new file mode 100644 index 0000000000..084040f27c --- /dev/null +++ b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs @@ -0,0 +1,64 @@ +namespace Microsoft.ApplicationInsights.Processors +{ + using System.Diagnostics; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Internal; + using OpenTelemetry; + + /// + /// An activity processor that applies client-level properties + /// to all activities as tags, using skip-if-present semantics. + /// This ensures context tags are applied universally — to Track* calls, Start/Stop operations, + /// and any OpenTelemetry API activities emitted by customer code. + /// + internal sealed class TelemetryContextActivityProcessor : BaseProcessor + { + private readonly TelemetryContext context; + + /// + /// Initializes a new instance of the class. + /// + /// The client-level to apply. + public TelemetryContextActivityProcessor(TelemetryContext context) + { + this.context = context; + } + + /// + /// Called when an activity ends. Applies client-level context tags using + /// skip-if-present semantics: if a tag is already set (by item-level context, + /// Track* methods, or instrumentation), it is not overwritten. + /// This matches the 2.x SDK's Tags.CopyTagValue precedence behavior. + /// + /// The activity that ended. + public override void OnEnd(Activity activity) + { + if (activity == null || this.context == null) + { + return; + } + + SetTagIfAbsent(activity, SemanticConventions.AttributeEnduserPseudoId, this.context.User?.Id); + SetTagIfAbsent(activity, SemanticConventions.AttributeEnduserId, this.context.User?.AuthenticatedUserId); + SetTagIfAbsent(activity, SemanticConventions.AttributeMicrosoftOperationName, this.context.Operation?.Name); + SetTagIfAbsent(activity, SemanticConventions.AttributeMicrosoftClientIp, this.context.Location?.Ip); + SetTagIfAbsent(activity, SemanticConventions.AttributeMicrosoftSessionId, this.context.Session?.Id); + SetTagIfAbsent(activity, SemanticConventions.AttributeAiDeviceId, this.context.Device?.Id); + SetTagIfAbsent(activity, SemanticConventions.AttributeAiDeviceModel, this.context.Device?.Model); + SetTagIfAbsent(activity, SemanticConventions.AttributeAiDeviceType, this.context.Device?.Type); + SetTagIfAbsent(activity, SemanticConventions.AttributeAiDeviceOsVersion, this.context.Device?.OperatingSystem); + SetTagIfAbsent(activity, SemanticConventions.AttributeMicrosoftSyntheticSource, this.context.Operation?.SyntheticSource); + SetTagIfAbsent(activity, SemanticConventions.AttributeMicrosoftUserAccountId, this.context.User?.AccountId); + + base.OnEnd(activity); + } + + private static void SetTagIfAbsent(Activity activity, string key, string value) + { + if (!string.IsNullOrEmpty(value) && activity.GetTagItem(key) == null) + { + activity.SetTag(key, value); + } + } + } +} diff --git a/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs new file mode 100644 index 0000000000..136f6e7699 --- /dev/null +++ b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs @@ -0,0 +1,103 @@ +namespace Microsoft.ApplicationInsights.Processors +{ + using System.Collections.Generic; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Internal; + using OpenTelemetry; + using OpenTelemetry.Logs; + + /// + /// A log processor that applies client-level properties + /// to all log records as attributes, using skip-if-present semantics. + /// This ensures context attributes are applied universally — to Track* calls + /// and any calls from customer code. + /// + internal sealed class TelemetryContextLogProcessor : BaseProcessor + { + private readonly TelemetryContext context; + + /// + /// Initializes a new instance of the class. + /// + /// The client-level to apply. + public TelemetryContextLogProcessor(TelemetryContext context) + { + this.context = context; + } + + /// + /// Called when a log record ends. Applies client-level context attributes using + /// skip-if-present semantics: if an attribute key is already present in the log record, + /// it is not overwritten. This matches the 2.x SDK's Tags.CopyTagValue precedence behavior. + /// The Azure Monitor Exporter's LogsHelper.ProcessLogRecordProperties will pick up + /// these attributes and route them into LogContextInfo for context tag mapping. + /// + /// The log record being processed. + public override void OnEnd(LogRecord logRecord) + { + if (logRecord == null || this.context == null) + { + return; + } + + // Collect existing attribute keys to check for presence + var existingKeys = new HashSet(); + if (logRecord.Attributes != null) + { + foreach (var attr in logRecord.Attributes) + { + existingKeys.Add(attr.Key); + } + } + + // Build list of context attributes to add (only those not already present) + var contextAttributes = new List>(); + AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeEnduserPseudoId, this.context.User?.Id); + AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeEnduserId, this.context.User?.AuthenticatedUserId); + AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeMicrosoftOperationName, this.context.Operation?.Name); + AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeMicrosoftClientIp, this.context.Location?.Ip); + AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeMicrosoftSessionId, this.context.Session?.Id); + AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeAiDeviceId, this.context.Device?.Id); + AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeAiDeviceModel, this.context.Device?.Model); + AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeAiDeviceType, this.context.Device?.Type); + AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeAiDeviceOsVersion, this.context.Device?.OperatingSystem); + AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeMicrosoftSyntheticSource, this.context.Operation?.SyntheticSource); + AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeMicrosoftUserAccountId, this.context.User?.AccountId); + + if (contextAttributes.Count == 0) + { + base.OnEnd(logRecord); + return; + } + + // Merge original attributes with context attributes into a new list + var mergedAttributes = new List>( + (logRecord.Attributes?.Count ?? 0) + contextAttributes.Count); + + if (logRecord.Attributes != null) + { + foreach (var attr in logRecord.Attributes) + { + mergedAttributes.Add(attr); + } + } + + mergedAttributes.AddRange(contextAttributes); + logRecord.Attributes = mergedAttributes; + + base.OnEnd(logRecord); + } + + private static void AddIfAbsent( + List> contextAttributes, + HashSet existingKeys, + string key, + string value) + { + if (!string.IsNullOrEmpty(value) && !existingKeys.Contains(key)) + { + contextAttributes.Add(new KeyValuePair(key, value)); + } + } + } +} diff --git a/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs b/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs index 4fa128979a..8394edfeb8 100644 --- a/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs +++ b/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs @@ -1120,8 +1120,6 @@ public Metric GetMetric( return new Metric(this, metricIdentifier.MetricId, metricIdentifier.MetricNamespace, dimensionNames); } - - private static LogLevel GetLogLevel(SeverityLevel severityLevel) { return severityLevel switch From e4a879307fc3d5f03f2ed00fb405f7606333e77d Mon Sep 17 00:00:00 2001 From: Harsimar Kaur Date: Fri, 13 Mar 2026 16:21:34 -0700 Subject: [PATCH 07/15] more changes, figure out why logging processor doesn't work in tests but does in example apps --- .../TelemetryClientContextTagsTest.cs | 392 ++++++++---------- .../Extensibility/TelemetryConfiguration.cs | 17 + .../OpenTelemetryBuilderExtensions.cs | 4 +- .../TelemetryContextActivityProcessor.cs | 10 + .../TelemetryContextLogProcessor.cs | 14 +- .../TelemetryClient.cs | 160 ++----- .../ApplicationInsightsExtensions.cs | 12 +- .../Controllers/HomeController.cs | 15 +- examples/BasicConsoleApp/Program.cs | 3 +- 9 files changed, 261 insertions(+), 366 deletions(-) diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs index c928413589..c140da802b 100644 --- a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs @@ -44,174 +44,10 @@ public void Dispose() this.activityItems?.Clear(); this.logItems?.Clear(); this.telemetryClient?.TelemetryConfiguration?.Dispose(); - } - - #region BuildContextTags — Attribute Key Mapping - - [Fact] - public void BuildContextTags_UserIdMapsToEnduserPseudoId() - { - this.telemetryClient.Context.User.Id = "user-123"; - - var tags = this.telemetryClient.ContextTags; - - Assert.True(tags.ContainsKey("enduser.pseudo.id")); - Assert.Equal("user-123", tags["enduser.pseudo.id"]); - } - - [Fact] - public void BuildContextTags_AuthenticatedUserIdMapsToEnduserId() - { - this.telemetryClient.Context.User.AuthenticatedUserId = "auth-456"; - - var tags = this.telemetryClient.ContextTags; - - Assert.True(tags.ContainsKey("enduser.id")); - Assert.Equal("auth-456", tags["enduser.id"]); - } - - [Fact] - public void BuildContextTags_OperationNameMapsToMicrosoftOperationName() - { - this.telemetryClient.Context.Operation.Name = "GET /api/users"; - - var tags = this.telemetryClient.ContextTags; - - Assert.True(tags.ContainsKey("microsoft.operation_name")); - Assert.Equal("GET /api/users", tags["microsoft.operation_name"]); - } - - [Fact] - public void BuildContextTags_LocationIpMapsToMicrosoftClientIp() - { - this.telemetryClient.Context.Location.Ip = "10.0.0.1"; - - var tags = this.telemetryClient.ContextTags; - - Assert.True(tags.ContainsKey("microsoft.client.ip")); - Assert.Equal("10.0.0.1", tags["microsoft.client.ip"]); - } - - [Fact] - public void BuildContextTags_AllPropertiesSet_ContainsAllAttributes() - { - this.telemetryClient.Context.User.Id = "user-1"; - this.telemetryClient.Context.User.AuthenticatedUserId = "auth-1"; - this.telemetryClient.Context.User.AccountId = "acct-1"; - this.telemetryClient.Context.Operation.Name = "TestOp"; - this.telemetryClient.Context.Operation.SyntheticSource = "bot"; - this.telemetryClient.Context.Location.Ip = "192.168.1.1"; - this.telemetryClient.Context.Session.Id = "session-1"; - this.telemetryClient.Context.Device.Id = "device-1"; - this.telemetryClient.Context.Device.Model = "Surface Pro"; - this.telemetryClient.Context.Device.Type = "Tablet"; - this.telemetryClient.Context.Device.OperatingSystem = "Windows 11"; - - var tags = this.telemetryClient.ContextTags; - - Assert.Equal(11, tags.Count); - Assert.Equal("user-1", tags["enduser.pseudo.id"]); - Assert.Equal("auth-1", tags["enduser.id"]); - Assert.Equal("TestOp", tags["microsoft.operation_name"]); - Assert.Equal("192.168.1.1", tags["microsoft.client.ip"]); - Assert.Equal("session-1", tags["microsoft.session.id"]); - Assert.Equal("device-1", tags["ai.device.id"]); - Assert.Equal("Surface Pro", tags["ai.device.model"]); - Assert.Equal("Tablet", tags["ai.device.type"]); - Assert.Equal("Windows 11", tags["ai.device.osVersion"]); - Assert.Equal("bot", tags["microsoft.synthetic_source"]); - Assert.Equal("acct-1", tags["microsoft.user.account_id"]); - } - - #endregion - - #region BuildContextTags — Null/Empty Exclusion - - [Fact] - public void BuildContextTags_NullUserIdExcluded() - { - this.telemetryClient.Context.User.Id = null; - this.telemetryClient.Context.Location.Ip = "10.0.0.1"; - - var tags = this.telemetryClient.ContextTags; - - Assert.False(tags.ContainsKey("enduser.pseudo.id")); - Assert.True(tags.ContainsKey("microsoft.client.ip")); - } - - [Fact] - public void BuildContextTags_EmptyStringExcluded() - { - this.telemetryClient.Context.User.Id = string.Empty; - this.telemetryClient.Context.User.AuthenticatedUserId = ""; - this.telemetryClient.Context.User.AccountId = ""; - this.telemetryClient.Context.Operation.Name = ""; - this.telemetryClient.Context.Operation.SyntheticSource = ""; - this.telemetryClient.Context.Location.Ip = ""; - this.telemetryClient.Context.Session.Id = ""; - this.telemetryClient.Context.Device.Id = ""; - this.telemetryClient.Context.Device.Model = ""; - this.telemetryClient.Context.Device.Type = ""; - this.telemetryClient.Context.Device.OperatingSystem = ""; - - var tags = this.telemetryClient.ContextTags; - - Assert.Equal(0, tags.Count); - } - - [Fact] - public void BuildContextTags_MixedSetAndUnset_OnlyIncludesNonEmpty() - { - this.telemetryClient.Context.User.Id = "user-1"; - // AuthenticatedUserId not set (null) - this.telemetryClient.Context.Operation.Name = "MyOp"; - // Location.Ip not set (null) - - var tags = this.telemetryClient.ContextTags; - - Assert.Equal(2, tags.Count); - Assert.True(tags.ContainsKey("enduser.pseudo.id")); - Assert.True(tags.ContainsKey("microsoft.operation_name")); - Assert.False(tags.ContainsKey("enduser.id")); - Assert.False(tags.ContainsKey("user_agent.original")); - Assert.False(tags.ContainsKey("microsoft.client.ip")); - } - - [Fact] - public void BuildContextTags_NoContextSet_ReturnsEmptyDictionary() - { - // Context is default (all properties null) - var tags = this.telemetryClient.ContextTags; - - Assert.NotNull(tags); - Assert.Equal(0, tags.Count); - } - - #endregion - - + } #region Log-based Telemetry — Context Tags Applied - [Fact] - public void TrackEvent_String_IncludesClientContextTags() - { - this.telemetryClient.Context.User.Id = "ctx-user"; - this.telemetryClient.Context.Location.Ip = "10.0.0.1"; - - this.telemetryClient.TrackEvent("TestEvent"); - this.telemetryClient.Flush(); - - var logRecord = this.logItems.FirstOrDefault(l => - l.Attributes != null && l.Attributes.Any(a => - a.Key == "microsoft.custom_event.name" && a.Value?.ToString() == "TestEvent")); - Assert.NotNull(logRecord); - - var attributes = logRecord.Attributes.ToDictionary(a => a.Key, a => a.Value?.ToString()); - Assert.Equal("ctx-user", attributes["enduser.pseudo.id"]); - Assert.Equal("10.0.0.1", attributes["microsoft.client.ip"]); - } - [Fact] public void TrackEvent_IncludesAllClientContextTags() { @@ -249,20 +85,6 @@ public void TrackEvent_IncludesAllClientContextTags() Assert.Equal("evt-acct", attributes["microsoft.user.account_id"]); } - [Fact] - public void TrackTrace_String_IncludesClientContextTags() - { - this.telemetryClient.Context.User.Id = "trace-user"; - - this.telemetryClient.TrackTrace("hello"); - this.telemetryClient.Flush(); - - Assert.True(this.logItems.Count > 0); - var logRecord = this.logItems.Last(); - var attributes = logRecord.Attributes.ToDictionary(a => a.Key, a => a.Value?.ToString()); - Assert.Equal("trace-user", attributes["enduser.pseudo.id"]); - } - [Fact] public void TrackTrace_WithSeverityAndProperties_IncludesClientContextTags() { @@ -367,27 +189,6 @@ public void TrackAvailability_IncludesClientContextTags() #region Log-based Telemetry — 3-Tier Priority Merge - [Fact] - public void TrackEvent_GlobalPropertiesOverrideContextTags() - { - // Client context (lowest priority) - this.telemetryClient.Context.User.Id = "context-user"; - - // GlobalProperties (medium priority) - override the same key - this.telemetryClient.Context.GlobalProperties["enduser.pseudo.id"] = "global-user"; - - this.telemetryClient.TrackEvent("TestEvent"); - this.telemetryClient.Flush(); - - var logRecord = this.logItems.FirstOrDefault(l => - l.Attributes != null && l.Attributes.Any(a => - a.Key == "microsoft.custom_event.name")); - Assert.NotNull(logRecord); - var attributes = logRecord.Attributes.ToDictionary(a => a.Key, a => a.Value?.ToString()); - - // GlobalProperties should override context tag - Assert.Equal("global-user", attributes["enduser.pseudo.id"]); - } [Fact] public void TrackEvent_ItemPropertiesOverrideContextTags() @@ -657,33 +458,6 @@ public void TrackDependency_ItemContextOverridesClientContext() Assert.True(activity.Tags.Any(t => t.Key == "enduser.pseudo.id" && t.Value == "client-user")); } - [Fact] - public void TrackRequest_ItemPropertiesOverrideGlobalPropertiesOverrideContextTags() - { - // Tier 1: Context tags (lowest) - this.telemetryClient.Context.User.Id = "ctx-user"; - - // Tier 2: GlobalProperties (medium) - this.telemetryClient.Context.GlobalProperties["enduser.pseudo.id"] = "global-user"; - this.telemetryClient.Context.GlobalProperties["env"] = "staging"; - - // Tier 3: Item properties (highest) - var request = new RequestTelemetry("GET /", DateTimeOffset.UtcNow, TimeSpan.FromMilliseconds(10), "200", true); - request.Properties["enduser.pseudo.id"] = "item-user"; - request.Properties["req-id"] = "r-1"; - - this.telemetryClient.TrackRequest(request); - this.telemetryClient.Flush(); - - Assert.Equal(1, this.activityItems.Count); - var activity = this.activityItems[0]; - - // Item > Global > Context - Assert.True(activity.Tags.Any(t => t.Key == "enduser.pseudo.id" && t.Value == "item-user")); - Assert.True(activity.Tags.Any(t => t.Key == "env" && t.Value == "staging")); - Assert.True(activity.Tags.Any(t => t.Key == "req-id" && t.Value == "r-1")); - } - [Fact] public void TrackDependency_ItemOperationNameOverridesClientOperationName() { @@ -713,6 +487,170 @@ public void TrackDependency_ItemOperationNameOverridesClientOperationName() #endregion + #region StartOperation — Context Tags Applied + + [Fact] + public void StartOperation_Request_AppliesClientContextTags() + { + this.telemetryClient.Context.User.Id = "op-user"; + this.telemetryClient.Context.Location.Ip = "10.0.0.5"; + this.telemetryClient.Context.Session.Id = "op-session"; + + using (var operation = this.telemetryClient.StartOperation("GET /api")) + { + // Activity is live — context processor runs on end + } + + this.telemetryClient.Flush(); + + Assert.True(this.activityItems.Count >= 1); + var activity = this.activityItems.First(a => a.DisplayName == "GET /api"); + Assert.True(activity.Tags.Any(t => t.Key == "enduser.pseudo.id" && t.Value == "op-user")); + Assert.True(activity.Tags.Any(t => t.Key == "microsoft.client.ip" && t.Value == "10.0.0.5")); + Assert.True(activity.Tags.Any(t => t.Key == "microsoft.session.id" && t.Value == "op-session")); + } + + [Fact] + public void StartOperation_Dependency_AppliesClientContextTags() + { + this.telemetryClient.Context.User.Id = "dep-op-user"; + this.telemetryClient.Context.Device.Id = "dep-op-device"; + + using (var operation = this.telemetryClient.StartOperation("SQL Query")) + { + } + + this.telemetryClient.Flush(); + + Assert.True(this.activityItems.Count >= 1); + var activity = this.activityItems.First(a => a.DisplayName == "SQL Query"); + Assert.True(activity.Tags.Any(t => t.Key == "enduser.pseudo.id" && t.Value == "dep-op-user")); + Assert.True(activity.Tags.Any(t => t.Key == "ai.device.id" && t.Value == "dep-op-device")); + } + + [Fact] + public void StartOperation_GlobalPropertiesApplied() + { + this.telemetryClient.Context.User.Id = "op-user"; + this.telemetryClient.Context.GlobalProperties["env"] = "prod"; + this.telemetryClient.Context.GlobalProperties["region"] = "westus"; + + using (var operation = this.telemetryClient.StartOperation("GET /health")) + { + } + + this.telemetryClient.Flush(); + + Assert.True(this.activityItems.Count >= 1); + var activity = this.activityItems.First(a => a.DisplayName == "GET /health"); + Assert.True(activity.Tags.Any(t => t.Key == "env" && t.Value == "prod")); + Assert.True(activity.Tags.Any(t => t.Key == "region" && t.Value == "westus")); + Assert.True(activity.Tags.Any(t => t.Key == "enduser.pseudo.id" && t.Value == "op-user")); + } + + #endregion + + #region GlobalProperties — Activity-based Telemetry + + [Fact] + public void TrackRequest_GlobalPropertiesAppliedAsTags() + { + this.telemetryClient.Context.GlobalProperties["env"] = "staging"; + this.telemetryClient.Context.GlobalProperties["deployment"] = "blue"; + + var request = new RequestTelemetry("GET /api", DateTimeOffset.UtcNow, TimeSpan.FromMilliseconds(10), "200", true); + this.telemetryClient.TrackRequest(request); + this.telemetryClient.Flush(); + + Assert.Equal(1, this.activityItems.Count); + var activity = this.activityItems[0]; + Assert.True(activity.Tags.Any(t => t.Key == "env" && t.Value == "staging")); + Assert.True(activity.Tags.Any(t => t.Key == "deployment" && t.Value == "blue")); + } + + [Fact] + public void TrackDependency_GlobalPropertiesAppliedAsTags() + { + this.telemetryClient.Context.GlobalProperties["tenant"] = "contoso"; + + var dep = new DependencyTelemetry { Type = "HTTP", Name = "GET /ext", Duration = TimeSpan.FromMilliseconds(50), Success = true }; + this.telemetryClient.TrackDependency(dep); + this.telemetryClient.Flush(); + + Assert.Equal(1, this.activityItems.Count); + Assert.True(this.activityItems[0].Tags.Any(t => t.Key == "tenant" && t.Value == "contoso")); + } + + [Fact] + public void TrackRequest_GlobalPropertiesDoNotOverrideItemProperties() + { + this.telemetryClient.Context.GlobalProperties["key1"] = "global-val"; + + var request = new RequestTelemetry("GET /", DateTimeOffset.UtcNow, TimeSpan.FromMilliseconds(10), "200", true); + request.Properties["key1"] = "item-val"; + this.telemetryClient.TrackRequest(request); + this.telemetryClient.Flush(); + + Assert.Equal(1, this.activityItems.Count); + // Item-level wins — SetTag from Track* runs before processor's SetTagIfAbsent + Assert.True(this.activityItems[0].Tags.Any(t => t.Key == "key1" && t.Value == "item-val")); + } + + #endregion + + #region GlobalProperties — Log-based Telemetry + + [Fact] + public void TrackEvent_GlobalPropertiesAppliedAsAttributes() + { + this.telemetryClient.Context.GlobalProperties["env"] = "production"; + this.telemetryClient.Context.GlobalProperties["version"] = "1.2.3"; + + this.telemetryClient.TrackEvent("GlobalPropEvent"); + this.telemetryClient.Flush(); + + var logRecord = this.logItems.FirstOrDefault(l => + l.Attributes != null && l.Attributes.Any(a => + a.Key == "microsoft.custom_event.name" && a.Value?.ToString() == "GlobalPropEvent")); + Assert.NotNull(logRecord); + var attributes = logRecord.Attributes.ToDictionary(a => a.Key, a => a.Value?.ToString()); + Assert.Equal("production", attributes["env"]); + Assert.Equal("1.2.3", attributes["version"]); + } + + [Fact] + public void TrackTrace_GlobalPropertiesAppliedAsAttributes() + { + this.telemetryClient.Context.GlobalProperties["component"] = "worker"; + + this.telemetryClient.TrackTrace("GlobalPropTrace"); + this.telemetryClient.Flush(); + + var logRecord = this.logItems.Last(); + var attributes = logRecord.Attributes.ToDictionary(a => a.Key, a => a.Value?.ToString()); + Assert.Equal("worker", attributes["component"]); + } + + [Fact] + public void TrackEvent_GlobalPropertiesDoNotOverrideItemProperties() + { + this.telemetryClient.Context.GlobalProperties["key1"] = "global-val"; + + var props = new Dictionary { { "key1", "item-val" } }; + this.telemetryClient.TrackEvent("OverrideEvent", props); + this.telemetryClient.Flush(); + + var logRecord = this.logItems.FirstOrDefault(l => + l.Attributes != null && l.Attributes.Any(a => + a.Key == "microsoft.custom_event.name" && a.Value?.ToString() == "OverrideEvent")); + Assert.NotNull(logRecord); + var attributes = logRecord.Attributes.ToDictionary(a => a.Key, a => a.Value?.ToString()); + // Item-level wins + Assert.Equal("item-val", attributes["key1"]); + } + + #endregion + #region Edge Cases [Fact] diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs index 5fdb3a10af..5db5a757b0 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs @@ -313,6 +313,23 @@ public void SetAzureTokenCredential(TokenCredential tokenCredential) }); } + /// + /// Prepends an additional OpenTelemetry builder configuration that runs before + /// any previously registered configurations. Used to ensure context processors + /// are registered before exporters so their OnEnd runs first. + /// + internal void PrependOpenTelemetryBuilder(Action configure) + { + this.ThrowIfBuilt(); + + var previousConfiguration = this.builderConfiguration; + this.builderConfiguration = builder => + { + configure(builder); + previousConfiguration(builder); + }; + } + /// /// Sets the cloud role name and role instance for telemetry. /// This configures the OpenTelemetry Resource with service.name, service.namespace, service.instance.id, and service.version attributes diff --git a/BASE/src/Microsoft.ApplicationInsights/OpenTelemetryBuilderExtensions.cs b/BASE/src/Microsoft.ApplicationInsights/OpenTelemetryBuilderExtensions.cs index 2ff7e8187e..285d0b2adc 100644 --- a/BASE/src/Microsoft.ApplicationInsights/OpenTelemetryBuilderExtensions.cs +++ b/BASE/src/Microsoft.ApplicationInsights/OpenTelemetryBuilderExtensions.cs @@ -32,9 +32,9 @@ public static IOpenTelemetryBuilder WithApplicationInsights(this IOpenTelemetryB new KeyValuePair("telemetry.distro.name", "Microsoft.ApplicationInsights"), new KeyValuePair("telemetry.distro.version", VersionUtils.GetVersion(typeof(OpenTelemetryBuilderExtensions))), })) - .WithLogging() .WithMetrics(metrics => metrics.AddMeter(TelemetryConfiguration.ApplicationInsightsMeterName)) - .WithTracing(tracing => tracing.AddSource(TelemetryConfiguration.ApplicationInsightsActivitySourceName)); + .WithTracing(tracing => tracing.AddSource(TelemetryConfiguration.ApplicationInsightsActivitySourceName)) + .WithLogging(); // Ensure that all log severity levels (including Verbose/Debug) pass through // the internal LoggerFactory for the TelemetryClient category. Without this, diff --git a/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs index 084040f27c..17b8abe63d 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs @@ -38,6 +38,16 @@ public override void OnEnd(Activity activity) return; } + // Apply client-level GlobalProperties (lowest priority — will not overwrite existing tags) + var globalProperties = this.context.GlobalPropertiesValue; + if (globalProperties != null) + { + foreach (var kvp in globalProperties) + { + SetTagIfAbsent(activity, kvp.Key, kvp.Value); + } + } + SetTagIfAbsent(activity, SemanticConventions.AttributeEnduserPseudoId, this.context.User?.Id); SetTagIfAbsent(activity, SemanticConventions.AttributeEnduserId, this.context.User?.AuthenticatedUserId); SetTagIfAbsent(activity, SemanticConventions.AttributeMicrosoftOperationName, this.context.Operation?.Name); diff --git a/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs index 136f6e7699..9a91399dce 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs @@ -50,8 +50,18 @@ public override void OnEnd(LogRecord logRecord) } } - // Build list of context attributes to add (only those not already present) + // Apply client-level GlobalProperties (lowest priority — will not overwrite existing keys) var contextAttributes = new List>(); + var globalProperties = this.context.GlobalPropertiesValue; + if (globalProperties != null) + { + foreach (var kvp in globalProperties) + { + AddIfAbsent(contextAttributes, existingKeys, kvp.Key, kvp.Value); + } + } + + // Build list of structured context attributes to add (only those not already present) AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeEnduserPseudoId, this.context.User?.Id); AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeEnduserId, this.context.User?.AuthenticatedUserId); AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeMicrosoftOperationName, this.context.Operation?.Name); @@ -94,7 +104,7 @@ private static void AddIfAbsent( string key, string value) { - if (!string.IsNullOrEmpty(value) && !existingKeys.Contains(key)) + if (!string.IsNullOrEmpty(value) && existingKeys.Add(key)) { contextAttributes.Add(new KeyValuePair(key, value)); } diff --git a/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs b/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs index 8394edfeb8..c4e9bfc3a6 100644 --- a/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs +++ b/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs @@ -14,6 +14,7 @@ using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing; using Microsoft.ApplicationInsights.Internal; using Microsoft.ApplicationInsights.Metrics; + using Microsoft.ApplicationInsights.Processors; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OpenTelemetry; @@ -57,10 +58,17 @@ internal TelemetryClient(TelemetryConfiguration configuration, bool isFromDepend // Apply CloudContext to Resource if set this.ApplyCloudContextToResource(); - // For non-DI scenarios: Build SDK eagerly to ensure TracerProvider is ready - // For DI scenarios: SDK will be built by configuration when accessed + // For non-DI scenarios: Register context processors and build SDK eagerly + // For DI scenarios: SDK will be built by configuration when accessed; + // processors are registered via UseApplicationInsightsTelemetry() in NETCORE if (!isFromDependencyInjection) { + configuration.ConfigureOpenTelemetryBuilder(builder => + { + builder.WithTracing(tracing => tracing.AddProcessor(new TelemetryContextActivityProcessor(this.Context))); + builder.WithLogging(logging => logging.AddProcessor(new TelemetryContextLogProcessor(this.Context))); + }); + this.sdk = configuration.Build(); } @@ -128,12 +136,6 @@ internal ILogger Logger } } - /// - /// Gets the context tags built from TelemetryClient.Context public properties. - /// Computed fresh on each access to reflect the latest Context values. - /// - internal IReadOnlyDictionary ContextTags => this.BuildContextTags(); - /// /// Check to determine if the tracking is enabled. /// @@ -161,7 +163,7 @@ public void TrackEvent(string eventName, IDictionary properties var mergedProperties = EnsureMutable(properties); mergedProperties["microsoft.custom_event.name"] = eventName; - var state = new DictionaryLogState(this.Context, this.ContextTags, mergedProperties, String.Empty); + var state = new DictionaryLogState(mergedProperties, String.Empty); this.Logger.Log(LogLevel.Information, 0, state, null, (s, ex) => s.Message); } @@ -188,7 +190,7 @@ public void TrackEvent(EventTelemetry telemetry) // Map item-level context properties to semantic conventions ApplyContextToProperties(telemetry.Context, mergedProperties); - var state = new DictionaryLogState(telemetry.Context, this.ContextTags, mergedProperties, String.Empty); + var state = new DictionaryLogState(telemetry.Context, mergedProperties, String.Empty); this.Logger.Log(LogLevel.Information, 0, state, null, (s, ex) => s.Message); } @@ -262,7 +264,7 @@ public void TrackAvailability(AvailabilityTelemetry telemetry) // Map item-level context properties to semantic conventions ApplyContextToProperties(telemetry.Context, properties); - var state = new DictionaryLogState(telemetry.Context, this.ContextTags, properties, telemetry.Message ?? String.Empty); + var state = new DictionaryLogState(telemetry.Context, properties, telemetry.Message ?? String.Empty); this.Logger.Log(LogLevel.Information, 0, state, null, (s, ex) => s.Message); } @@ -276,7 +278,7 @@ public void TrackAvailability(AvailabilityTelemetry telemetry) public void TrackTrace(string message) { this.Configuration.FeatureReporter.MarkFeatureInUse(StatsbeatFeatures.TrackTrace); - var state = new DictionaryLogState(this.Context, this.ContextTags, null, message); + var state = new DictionaryLogState(null, message); this.Logger.Log(LogLevel.Information, 0, state, null, (s, ex) => s.Message); } @@ -292,7 +294,7 @@ public void TrackTrace(string message, SeverityLevel severityLevel) { this.Configuration.FeatureReporter.MarkFeatureInUse(StatsbeatFeatures.TrackTrace); LogLevel logLevel = GetLogLevel(severityLevel); - var state = new DictionaryLogState(this.Context, this.ContextTags, null, message); + var state = new DictionaryLogState(null, message); this.Logger.Log(logLevel, 0, state, null, (s, ex) => s.Message); } @@ -307,7 +309,7 @@ public void TrackTrace(string message, SeverityLevel severityLevel) public void TrackTrace(string message, IDictionary properties) { this.Configuration.FeatureReporter.MarkFeatureInUse(StatsbeatFeatures.TrackTrace); - var state = new DictionaryLogState(this.Context, this.ContextTags, properties, message); + var state = new DictionaryLogState(properties, message); this.Logger.Log(LogLevel.Information, 0, state, null, (s, ex) => s.Message); } @@ -324,7 +326,7 @@ public void TrackTrace(string message, SeverityLevel severityLevel, IDictionary< { this.Configuration.FeatureReporter.MarkFeatureInUse(StatsbeatFeatures.TrackTrace); LogLevel logLevel = GetLogLevel(severityLevel); - var state = new DictionaryLogState(this.Context, this.ContextTags, properties, message); + var state = new DictionaryLogState(properties, message); this.Logger.Log(logLevel, 0, state, null, (s, ex) => s.Message); } @@ -359,7 +361,7 @@ public void TrackTrace(TraceTelemetry telemetry) ApplyContextToProperties(telemetry.Context, mergedProperties); LogLevel logLevel = GetLogLevel(telemetry.SeverityLevel.Value); - var state = new DictionaryLogState(telemetry.Context, this.ContextTags, mergedProperties, telemetry.Message); + var state = new DictionaryLogState(telemetry.Context, mergedProperties, telemetry.Message); this.Logger.Log(logLevel, 0, state, null, (s, ex) => s.Message); } @@ -451,7 +453,7 @@ public void TrackException(Exception exception, IDictionary prop exception = new InvalidOperationException(Utils.PopulateRequiredStringValue(null, "message", typeof(ExceptionTelemetry).FullName)); } - var state = new DictionaryLogState(this.Context, this.ContextTags, properties, exception.Message); + var state = new DictionaryLogState(properties, exception.Message); this.Logger.Log(LogLevel.Error, 0, state, exception, (s, ex) => s.Message); } @@ -478,7 +480,7 @@ public void TrackException(ExceptionTelemetry telemetry) var mergedProperties = EnsureMutable(telemetry.Properties); ApplyContextToProperties(telemetry.Context, mergedProperties); - var state = new DictionaryLogState(telemetry.Context, this.ContextTags, mergedProperties, reconstructedException.Message); + var state = new DictionaryLogState(telemetry.Context, mergedProperties, reconstructedException.Message); var logLevel = GetLogLevel(telemetry.SeverityLevel ?? SeverityLevel.Error); this.Logger.Log(logLevel, 0, state, reconstructedException, (s, ex) => s.Message); } @@ -606,26 +608,11 @@ public void TrackDependency(DependencyTelemetry telemetry) dependencyTelemetryActivity.SetTag(SemanticConventions.AttributeMicrosoftDependencyResultCode, telemetry.ResultCode); } - // Apply client-level context tags (lowest priority — applied first so higher layers override) - foreach (var tag in this.ContextTags) - { - dependencyTelemetryActivity.SetTag(tag.Key, tag.Value); - } - if (!string.IsNullOrEmpty(telemetry.Context?.Operation?.Name)) { dependencyTelemetryActivity.SetTag(SemanticConventions.AttributeMicrosoftOperationName, telemetry.Context.Operation.Name); } - // Add GlobalProperties first (lower priority) - if (this.Context?.GlobalPropertiesValue != null) - { - foreach (var property in this.Context.GlobalPropertiesValue) - { - dependencyTelemetryActivity.SetTag(property.Key, property.Value); - } - } - // Add telemetry context GlobalProperties if (telemetry.Context?.GlobalPropertiesValue != null) { @@ -790,26 +777,11 @@ public void TrackRequest(RequestTelemetry request) activity.SetTag(SemanticConventions.AttributeMicrosoftRequestResultCode, request.ResponseCode); } - // Apply client-level context tags (lowest priority — applied first so higher layers override) - foreach (var tag in this.ContextTags) - { - activity.SetTag(tag.Key, tag.Value); - } - if (!string.IsNullOrEmpty(request.Context?.Operation?.Name)) { activity.SetTag(SemanticConventions.AttributeMicrosoftOperationName, request.Context.Operation.Name); } - // Add GlobalProperties first (lower priority) - if (this.Context?.GlobalPropertiesValue != null) - { - foreach (var property in this.Context.GlobalPropertiesValue) - { - activity.SetTag(property.Key, property.Value); - } - } - // Add request context GlobalProperties if (request.Context?.GlobalPropertiesValue != null) { @@ -1515,73 +1487,6 @@ private void ApplyCloudContextToResource() } } - /// - /// Builds a dictionary of context tags from public TelemetryClient.Context properties. - /// Computed fresh on each call to reflect the latest Context values. - /// Only includes properties that are not null or empty. - /// - private Dictionary BuildContextTags() - { - var tags = new Dictionary(); - - if (!string.IsNullOrEmpty(this.Context.User?.Id)) - { - tags[SemanticConventions.AttributeEnduserPseudoId] = this.Context.User.Id; - } - - if (!string.IsNullOrEmpty(this.Context.User?.AuthenticatedUserId)) - { - tags[SemanticConventions.AttributeEnduserId] = this.Context.User.AuthenticatedUserId; - } - - if (!string.IsNullOrEmpty(this.Context.Operation?.Name)) - { - tags[SemanticConventions.AttributeMicrosoftOperationName] = this.Context.Operation.Name; - } - - if (!string.IsNullOrEmpty(this.Context.Location?.Ip)) - { - tags[SemanticConventions.AttributeMicrosoftClientIp] = this.Context.Location.Ip; - } - - if (!string.IsNullOrEmpty(this.Context.Session?.Id)) - { - tags[SemanticConventions.AttributeMicrosoftSessionId] = this.Context.Session.Id; - } - - if (!string.IsNullOrEmpty(this.Context.Device?.Id)) - { - tags[SemanticConventions.AttributeAiDeviceId] = this.Context.Device.Id; - } - - if (!string.IsNullOrEmpty(this.Context.Device?.Model)) - { - tags[SemanticConventions.AttributeAiDeviceModel] = this.Context.Device.Model; - } - - if (!string.IsNullOrEmpty(this.Context.Device?.Type)) - { - tags[SemanticConventions.AttributeAiDeviceType] = this.Context.Device.Type; - } - - if (!string.IsNullOrEmpty(this.Context.Device?.OperatingSystem)) - { - tags[SemanticConventions.AttributeAiDeviceOsVersion] = this.Context.Device.OperatingSystem; - } - - if (!string.IsNullOrEmpty(this.Context.Operation?.SyntheticSource)) - { - tags[SemanticConventions.AttributeMicrosoftSyntheticSource] = this.Context.Operation.SyntheticSource; - } - - if (!string.IsNullOrEmpty(this.Context.User?.AccountId)) - { - tags[SemanticConventions.AttributeMicrosoftUserAccountId] = this.Context.User.AccountId; - } - - return tags; - } - private readonly struct DictionaryLogState : IReadOnlyList> { public readonly string Message; @@ -1609,29 +1514,20 @@ public DictionaryLogState(IDictionary properties, string message } /// - /// Constructor that merges TelemetryContext.GlobalProperties with client-level context tags and item-level properties. + /// Constructor that merges TelemetryContext.GlobalProperties with item-level properties. + /// Client-level context tags are handled by . /// /// The telemetry context containing GlobalProperties. - /// Client-level cached context tags (lowest priority). - /// Item-level properties (highest priority, override all). + /// Item-level properties (highest priority, override GlobalProperties). /// The log message. - public DictionaryLogState(TelemetryContext context, IReadOnlyDictionary clientContextTags, IDictionary properties, string message) + public DictionaryLogState(TelemetryContext context, IDictionary properties, string message) { this.Message = message ?? string.Empty; - // Merge in priority order: clientContextTags (lowest) -> GlobalProperties -> properties (highest) + // Merge in priority order: GlobalProperties (lower) -> properties (higher) var allProperties = new Dictionary(); - // 1. Client-level cached context tags (lowest priority) - if (clientContextTags != null) - { - foreach (var kvp in clientContextTags) - { - allProperties[kvp.Key] = kvp.Value; - } - } - - // 2. GlobalProperties from context (override client-level) + // 1. GlobalProperties from context if (context?.GlobalProperties != null) { foreach (var kvp in context.GlobalProperties) @@ -1640,12 +1536,12 @@ public DictionaryLogState(TelemetryContext context, IReadOnlyDictionary()); + .AddProcessor() + .AddProcessor(sp => + new TelemetryContextActivityProcessor(sp.GetRequiredService().Context))); // Register ActivityFilterProcessor in DI builder.Services.AddSingleton(); + builder.Services.ConfigureOpenTelemetryLoggerProvider((sp, loggerBuilder) => + { + loggerBuilder.AddProcessor( + new TelemetryContextLogProcessor(sp.GetRequiredService().Context)); + }); + builder.WithMetrics(b => b.AddHttpClientAndServerMetrics()); builder.Services.AddOptions() diff --git a/examples/AspNetCoreWebApp/Controllers/HomeController.cs b/examples/AspNetCoreWebApp/Controllers/HomeController.cs index 8588d30c2a..e58695c2e8 100644 --- a/examples/AspNetCoreWebApp/Controllers/HomeController.cs +++ b/examples/AspNetCoreWebApp/Controllers/HomeController.cs @@ -20,6 +20,7 @@ public HomeController(ILogger logger, TelemetryClient telemetryC { this._logger = logger; this._telemetryClient = telemetryClient; + telemetryClient.Context.User.Id = "TestUserId"; // In a real app, you wouldn't need the TelemetryConfiguration here. // This is included in this sample because it allows you to debug and verify that the configuration at runtime matches the expected configuration. @@ -28,9 +29,21 @@ public HomeController(ILogger logger, TelemetryClient telemetryC public IActionResult Index() { - // this._telemetryClient.TrackEvent(eventName: "Hello World!"); + this._telemetryClient.TrackEvent(eventName: "Hello World!"); + this._telemetryClient.TrackTrace(message: "This is a trace message."); + this._telemetryClient.TrackException(exception: new Exception("This is a test exception.")); + this._telemetryClient.TrackDependency(dependencyTypeName: "HTTP", target: "www.example.com", dependencyName: "GET /api/test", data: null, startTime: DateTimeOffset.Now, duration: TimeSpan.FromMilliseconds(100), resultCode: "200", success: true); this._telemetryClient.TrackRequest("Test Request", DateTimeOffset.Now, TimeSpan.FromMilliseconds(123), "200", true); + _logger.LogInformation("Hello from HomeController.Index!"); + _logger.LogError(new Exception("This is a test exception logged with ILogger."), "This is a test exception logged with ILogger."); + + using (var operation = this._telemetryClient.StartOperation("TestOperation")) + { + operation.Telemetry.Properties["CustomProperty"] = "CustomValue"; + // Simulate some work + System.Threading.Thread.Sleep(100); + } return View(); } diff --git a/examples/BasicConsoleApp/Program.cs b/examples/BasicConsoleApp/Program.cs index eb92e86943..ff29525339 100644 --- a/examples/BasicConsoleApp/Program.cs +++ b/examples/BasicConsoleApp/Program.cs @@ -19,7 +19,8 @@ static void Main(string[] args) { var telemetryConfig = TelemetryConfiguration.CreateDefault(); - telemetryConfig.ConnectionString = ""; + telemetryConfig.ConnectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://westus2-0.in.applicationinsights.azure.com/;LiveEndpoint=https://westus2.livediagnostics.monitor.azure.com/"; + telemetryConfig.SamplingRatio = 1.0; // Set to 100% for testing; adjust as needed for production telemetryConfig.ConfigureOpenTelemetryBuilder(builder => builder.WithTracing(tracing => tracing.AddSource("MyCompany.MyProduct.MyLibrary").AddConsoleExporter()) .WithLogging(logging => logging.AddConsoleExporter()) From 485ff79b769a08c38ae975666e0bf8796e3a4173 Mon Sep 17 00:00:00 2001 From: Harsimar Kaur Date: Mon, 16 Mar 2026 16:39:47 -0700 Subject: [PATCH 08/15] cleanup --- .../DataContracts/TelemetryContext.cs | 2 - .../Implementation/OperationContext.cs | 9 +- .../Extensibility/TelemetryConfiguration.cs | 8 +- .../Microsoft.ApplicationInsights.csproj | 2 +- .../TelemetryClient.cs | 25 +----- BreakingChanges.md | 44 +++++----- MigrationGuidance.md | 83 +++++++++++-------- .../Controllers/HomeController.cs | 3 + examples/BasicConsoleApp/Program.cs | 5 +- 9 files changed, 91 insertions(+), 90 deletions(-) diff --git a/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs b/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs index efa779ad80..0e666a6d80 100644 --- a/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs @@ -81,9 +81,7 @@ public LocationContext Location /// public DeviceContext Device { -#pragma warning disable CS0618 // Type or member is obsolete get { return LazyInitializer.EnsureInitialized(ref this.device, () => new DeviceContext(default)); } -#pragma warning restore CS0618 // Type or member is obsolete } /// diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/OperationContext.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/OperationContext.cs index 9c64e8e1ad..f3643305a0 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/OperationContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/OperationContext.cs @@ -1,5 +1,7 @@ namespace Microsoft.ApplicationInsights.Extensibility.Implementation { + using System.ComponentModel; + /// /// Encapsulates information about an operation. Operation normally reflects an end to end scenario that starts from a user action (e.g. button click). /// @@ -34,7 +36,7 @@ public string SyntheticSource } /// - /// Gets or sets the application-defined operation ID. + /// Gets or sets the application-defined operation ID for the topmost operation. /// internal string Id { @@ -43,7 +45,7 @@ internal string Id } /// - /// Gets or sets the application-defined parent operation ID. + /// Gets or sets the parent operation ID. /// internal string ParentId { @@ -52,8 +54,9 @@ internal string ParentId } /// - /// Gets or sets the application-defined correlation vector. + /// Gets or sets the correlation vector for the current telemetry item. /// + [EditorBrowsable(EditorBrowsableState.Never)] internal string CorrelationVector { get { return string.IsNullOrEmpty(this.correlationVector) ? null : this.correlationVector; } diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs index 5db5a757b0..540fc282d6 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs @@ -314,11 +314,11 @@ public void SetAzureTokenCredential(TokenCredential tokenCredential) } /// - /// Prepends an additional OpenTelemetry builder configuration that runs before - /// any previously registered configurations. Used to ensure context processors - /// are registered before exporters so their OnEnd runs first. + /// Prepends a configuration action so it runs before any user-registered configuration. + /// This ensures that enrichment processors (e.g., TelemetryContextLogProcessor) execute + /// before export processors, regardless of the order users call ConfigureOpenTelemetryBuilder. /// - internal void PrependOpenTelemetryBuilder(Action configure) + internal void PrependOpenTelemetryBuilderConfiguration(Action configure) { this.ThrowIfBuilt(); diff --git a/BASE/src/Microsoft.ApplicationInsights/Microsoft.ApplicationInsights.csproj b/BASE/src/Microsoft.ApplicationInsights/Microsoft.ApplicationInsights.csproj index 62a9079eb3..6649471f86 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Microsoft.ApplicationInsights.csproj +++ b/BASE/src/Microsoft.ApplicationInsights/Microsoft.ApplicationInsights.csproj @@ -27,7 +27,7 @@ - + All diff --git a/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs b/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs index c4e9bfc3a6..37b410c365 100644 --- a/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs +++ b/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs @@ -55,15 +55,14 @@ internal TelemetryClient(TelemetryConfiguration configuration, bool isFromDepend // Use the shared ActivitySource from configuration this.activitySource = configuration.ApplicationInsightsActivitySource; - // Apply CloudContext to Resource if set - this.ApplyCloudContextToResource(); - // For non-DI scenarios: Register context processors and build SDK eagerly // For DI scenarios: SDK will be built by configuration when accessed; // processors are registered via UseApplicationInsightsTelemetry() in NETCORE if (!isFromDependencyInjection) { - configuration.ConfigureOpenTelemetryBuilder(builder => + // Prepend context processors so they run BEFORE any user-registered exporters. + // This ensures LogRecords/Activities are enriched with context tags before export. + configuration.PrependOpenTelemetryBuilderConfiguration(builder => { builder.WithTracing(tracing => tracing.AddProcessor(new TelemetryContextActivityProcessor(this.Context))); builder.WithLogging(logging => logging.AddProcessor(new TelemetryContextLogProcessor(this.Context))); @@ -1469,24 +1468,6 @@ private static void ApplyContextToProperties(TelemetryContext context, IDictiona } } - /// - /// Applies CloudContext (RoleName/RoleInstance) and ComponentContext (Version) to the OpenTelemetry Resource if set. - /// - private void ApplyCloudContextToResource() - { - var roleName = this.Context.Cloud.RoleName; - var roleInstance = this.Context.Cloud.RoleInstance; - var componentVersion = this.Context.Component.Version; - - if (!string.IsNullOrEmpty(roleName) || !string.IsNullOrEmpty(roleInstance) || !string.IsNullOrEmpty(componentVersion)) - { - this.Configuration.SetCloudRole( - serviceName: roleName, - serviceInstanceId: roleInstance, - serviceVersion: componentVersion); - } - } - private readonly struct DictionaryLogState : IReadOnlyList> { public readonly string Message; diff --git a/BreakingChanges.md b/BreakingChanges.md index 61013e8204..56a6e9284c 100644 --- a/BreakingChanges.md +++ b/BreakingChanges.md @@ -134,7 +134,10 @@ Migration note: TelemetryConfiguration.CreateDefault is the recommended way to i ## TelemetryContext Breaking Changes -Several TelemetryContext sub-context classes have been made internal, and some properties on the remaining public sub-contexts have also been made internal. +Several TelemetryContext sub-context properties have been made internal, and some previously public properties have been removed. + +> [!IMPORTANT] +> Context properties set on `TelemetryClient.Context` are applied to all **traces and logs** (TrackRequest, TrackDependency, TrackTrace, TrackEvent, TrackException, TrackAvailability). **Metrics are not enriched** with context properties. ### TelemetryContext Properties Removed - **`InstrumentationKey`** - Removed. Use `TelemetryConfiguration.ConnectionString` instead. @@ -142,30 +145,27 @@ Several TelemetryContext sub-context classes have been made internal, and some p - **`Properties`** (obsolete) - Was obsoleted in 2.x in favor of `GlobalProperties`. - **`TryGetRawObject()`** / **`StoreRawObject()`** - Removed. These methods were used to pass raw objects between collectors and initializers, which no longer exist. -### Sub-Context Classes Made Internal -The following sub-context classes were **public** in 2.x and are now **internal** in 3.x. Their properties are no longer accessible: -- **`Cloud`** (`CloudContext`) — Had `RoleName`, `RoleInstance`. -- **`Component`** (`ComponentContext`) — Had `Version`. -- **`Device`** (`DeviceContext`) — Had `Type`, `Id`, `OperatingSystem`, `OemName`, `Model`. -- **`Session`** (`SessionContext`) — Had `Id`, `IsFirst`. - -See detailed migration guidance [here](MigrationGuidance.md#telemetry-context) - ### Sub-Context Properties Made Internal -The following properties on **still-public** sub-context classes have been made internal: -- **`User.UserAgent`** — Was public in 2.x, now internal. The Application Insights ingestion service does not surface this field in workspace-based resources. -- **`User.AccountId`** — Was public in 2.x, now internal. This can be set via adding properties to Track() calls or creating custom OpenTelemetry processors. -- **`Operation.Id`** — Was public in 2.x, now internal. Correlation IDs are managed automatically by OpenTelemetry. -- **`Operation.ParentId`** — Was public in 2.x, now internal. Correlation IDs are managed automatically by OpenTelemetry. -- **`Operation.CorrelationVector`** — No longer needed due to shift to OpenTelemetry correlation. -- **`Operation.SyntheticSource`** — There is future work planned to reset this to public. - -### Properties Retained -The following remain **public**: -- `User` (`Id`, `AuthenticatedUserId`) -- `Operation` (`Name`) +The following properties on sub-context classes have been made internal: +- **`User.UserAgent`** — Now internal. The Application Insights ingestion service does not surface this field in workspace-based resources. +- **`Device.OemName`** — Now internal. The Application Insights ingestion service does not surface this field in workspace-based resources. +- **`Session.IsFirst`** — Now internal. The Application Insights ingestion service does not surface this field in workspace-based resources. +- **`Operation.Id`** — Now internal. Correlation IDs are managed automatically by OpenTelemetry. +- **`Operation.ParentId`** — Now internal. Correlation IDs are managed automatically by OpenTelemetry. +- **`Operation.CorrelationVector`** — Now internal. No longer needed due to shift to OpenTelemetry correlation. + +### Properties Retained (Public) +All sub-context classes remain **public**. The following properties remain **public** and can be set on `TelemetryClient.Context` (applies to all non-metric telemetry) or on individual telemetry items: +- `Cloud` (`RoleName`, `RoleInstance`) +- `Component` (`Version`) +- `User` (`Id`, `AuthenticatedUserId`, `AccountId`) +- `Operation` (`Name`, `SyntheticSource`) - `Location` (`Ip`) +- `Session` (`Id`) +- `Device` (`Type`, `Id`, `OperatingSystem`, `Model`) - `GlobalProperties` + +See detailed migration guidance [here](MigrationGuidance.md#telemetry-context) --- # 2. Microsoft.ApplicationInsights.AspNetCore diff --git a/MigrationGuidance.md b/MigrationGuidance.md index 6d3ce90346..f5236bfb9e 100644 --- a/MigrationGuidance.md +++ b/MigrationGuidance.md @@ -320,28 +320,30 @@ There is not a direct replacement for the InMemoryChannel. However, OpenTelemetr Customers who implemented custom `ITelemetryChannel` for sending telemetry to additional backends should use OpenTelemetry exporters instead. See the [TelemetrySinks](#telemetrysinks) section for a console exporter example. ## Telemetry Context -As mentioned in [breaking changes](BreakingChanges.md#telemetrycontext-breaking-changes), many context classes and properties have been marked as internal. For each of the removed classes, workarounds are listed: +As mentioned in [breaking changes](BreakingChanges.md#telemetrycontext-breaking-changes), many context classes and properties have been marked as internal. The sections below describe how to set context in 3.x. -### Cloud: RoleName, RoleInstance & Component Version -To set these values on all telemetry items, configure the OpenTelemetry service attributes `service.name` (maps to Cloud RoleName) and `service.instance.id` (maps to Cloud RoleInstance) and `service.version` (maps to application version). +### Cloud RoleName, RoleInstance & Component Version + +These values map to OpenTelemetry Resource attributes (`service.name`, `service.instance.id`, `service.version`). Because Resource attributes are immutable after the OpenTelemetry SDK is built, the mechanism differs between non-DI and DI scenarios. #### Microsoft.ApplicationInsights (Base SDK) & Microsoft.ApplicationInsights.Web + +In non-DI scenarios, set these directly on the `TelemetryClient.Context` before sending telemetry. + ```csharp -var configuration = TelemetryConfiguration.CreateDefault(); -configuration.ConnectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://..."; +var config = TelemetryConfiguration.CreateDefault(); +config.ConnectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://..."; -configuration.ConfigureOpenTelemetryBuilder(builder => -{ - builder.ConfigureResource(r => r - .AddService( - serviceName: "MyRoleName", // Maps to Cloud.RoleName - serviceInstanceId: Environment.MachineName, // Maps to Cloud.RoleInstance - serviceVersion: "1.0.0") // Maps to Component.Version - ); -}); +var client = new TelemetryClient(config); +client.Context.Cloud.RoleName = "MyRoleName"; // Maps to Cloud.RoleName +client.Context.Cloud.RoleInstance = Environment.MachineName; // Maps to Cloud.RoleInstance +client.Context.Component.Version = "1.0.0"; // Maps to Component.Version ``` #### Microsoft.ApplicationInsights.AspNetCore & Microsoft.ApplicationInsights.WorkerService + +In DI scenarios, the OpenTelemetry SDK is built by the host before user code runs, so the `Context.Cloud` setters cannot influence the Resource in time. Instead, configure the Resource via the OpenTelemetry builder: + ```csharp builder.Services.ConfigureOpenTelemetryTracerProvider((sp, tracerBuilder) => { @@ -363,39 +365,52 @@ builder.Services.ConfigureOpenTelemetryLoggerProvider((sp, loggerBuilder) => ); }); ``` -If one wishes to set service attributes only on specific telemetry items, that can be done via Track* overloads that take in custom properties, or via setting the property on the telemetry item before passing it to a Track* call. - -> [!NOTE] -> There is a future work item to enable the setting of CloudContext and ComponentVersion properties without having to explicitly configure OpenTelemetry providers. -### Device and Session Context & User.AccountId -Currently, the Azure Monitor Exporter does not contain the mapping for these context attributes to appear in the correct part of the payload if a customer explicitly specifies it. -The current workaround is to add custom dimensions to your telemetry items instead. -For individual items, this can be done via Track* overloads that take in custom properties, or it can be set on the individual telemetry item via `Properties` before being passed to Track* calls. -To add dimensions to all telemetry items, consider this [alternative](#familiar-api-alternative) or a [custom OpenTelemetry processor](#creating-a-custom-opentelemetry-processor). +### User, Operation, Location, Device & Session Context -> [!NOTE] -> There is a future work item to enable the setting of these context properties via their original Context attributes. +These context properties can be set in two ways: on individual telemetry items, or on the `TelemetryClient.Context` to apply to all telemetry sent by that client. -### Setting TelemetryContext via TelemetryClient -In 2.x it was possible to set TelemetryContext on the TelemetryClient, such that the context appeared on every telemetry item. -In 3.x, this functionality is not completely implemented - ie, only the `GlobalProperties` will propagate. We are working on a fix to ensure other properties will also propagate. +> [!IMPORTANT] +> Context set on `TelemetryClient.Context` applies to all **traces and logs**. **Metrics are not enriched** with these context properties. -Intended syntax: +#### Setting context via TelemetryClient ```csharp var client = new TelemetryClient(config); -// GlobalProperties - this works today and will appear in customDimensions on all telemetry -client.Context.GlobalProperties["MyCustomGlobalProperty"] = "Production"; +// GlobalProperties appear in customDimensions +client.Context.GlobalProperties["Environment"] = "Production"; -// The following are public and settable on individual telemetry items today. -// There is a known issue where these do not yet propagate when set on TelemetryClient.Context. -// Once fixed, setting them here will apply to all telemetry sent by this client: +// These context properties are applied to all traces and logs sent by this client: client.Context.User.Id = "anonymous-user-id"; client.Context.User.AuthenticatedUserId = "authenticated-user-id"; +client.Context.User.AccountId = "account-123"; client.Context.Operation.Name = "MyOperation"; +client.Context.Operation.SyntheticSource = "BotTraffic"; client.Context.Location.Ip = "127.0.0.1"; +client.Context.Session.Id = "session-abc"; +client.Context.Device.Id = "device-xyz"; +client.Context.Device.Model = "Surface Pro"; +client.Context.Device.Type = "PC"; +client.Context.Device.OperatingSystem = "Windows 11"; +``` + +#### Setting context on an individual telemetry item + +Item-level context overrides client-level context for that item. This works for both activity-based telemetry (Request, Dependency) and log-based telemetry (Trace, Event, Exception, Availability). + +```csharp +// Activity-based: set context on the telemetry item before passing to Track* +var request = new RequestTelemetry("GET /api/orders", DateTimeOffset.Now, TimeSpan.FromMilliseconds(150), "200", true); +request.Context.User.Id = "specific-user"; +request.Context.Session.Id = "specific-session"; +client.TrackRequest(request); + +// Log-based: set context on the telemetry item before passing to Track* +var trace = new TraceTelemetry("Processing order"); +trace.Context.User.Id = "specific-user"; +trace.Context.Operation.Name = "OrderProcessing"; +client.TrackTrace(trace); ``` ## Changes to ApplicationInsightsServiceOptions diff --git a/examples/AspNetCoreWebApp/Controllers/HomeController.cs b/examples/AspNetCoreWebApp/Controllers/HomeController.cs index e58695c2e8..f56e0ed48a 100644 --- a/examples/AspNetCoreWebApp/Controllers/HomeController.cs +++ b/examples/AspNetCoreWebApp/Controllers/HomeController.cs @@ -21,6 +21,9 @@ public HomeController(ILogger logger, TelemetryClient telemetryC this._logger = logger; this._telemetryClient = telemetryClient; telemetryClient.Context.User.Id = "TestUserId"; + telemetryClient.Context.Cloud.RoleName = "TestRoleName"; + telemetryClient.Context.Cloud.RoleInstance = "TestRoleInstance"; + telemetryClient.Context.Component.Version = "2.0.0"; // In a real app, you wouldn't need the TelemetryConfiguration here. // This is included in this sample because it allows you to debug and verify that the configuration at runtime matches the expected configuration. diff --git a/examples/BasicConsoleApp/Program.cs b/examples/BasicConsoleApp/Program.cs index ff29525339..640e8d7414 100644 --- a/examples/BasicConsoleApp/Program.cs +++ b/examples/BasicConsoleApp/Program.cs @@ -19,8 +19,8 @@ static void Main(string[] args) { var telemetryConfig = TelemetryConfiguration.CreateDefault(); - telemetryConfig.ConnectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://westus2-0.in.applicationinsights.azure.com/;LiveEndpoint=https://westus2.livediagnostics.monitor.azure.com/"; - telemetryConfig.SamplingRatio = 1.0; // Set to 100% for testing; adjust as needed for production + telemetryConfig.ConnectionString = ""; + telemetryConfig.SamplingRatio = 1.0f; // Set to 100% for testing; adjust as needed for production telemetryConfig.ConfigureOpenTelemetryBuilder(builder => builder.WithTracing(tracing => tracing.AddSource("MyCompany.MyProduct.MyLibrary").AddConsoleExporter()) .WithLogging(logging => logging.AddConsoleExporter()) @@ -44,6 +44,7 @@ static void Main(string[] args) telemetryClient.Context.Cloud.RoleName = "TestRole"; telemetryClient.Context.Cloud.RoleInstance = "TestInstance"; telemetryClient.Context.Component.Version = "2.0.0"; + telemetryClient.Context.GlobalProperties["GlobalKey"] = "GlobalValue"; // **The following lines are examples of tracking different telemetry types.** From eb4db4db27fa7cd5e97802af236152014f6b2ab4 Mon Sep 17 00:00:00 2001 From: Rajkumar Rangaraj Date: Fri, 20 Mar 2026 13:25:51 -0700 Subject: [PATCH 09/15] Add TelemetryContextLogProcessorTests and optimize TelemetryContext processors - Introduced unit tests for TelemetryContextLogProcessor to validate attribute application, global properties handling, and snapshot behavior. - Enhanced TelemetryContextActivityProcessor and TelemetryContextLogProcessor with warmup optimizations, freezing snapshots for improved performance. - Implemented fast paths for attribute merging to reduce allocations and improve efficiency during logging operations. - Ensured thread safety and proper handling of existing attributes to prevent overwrites during concurrent processing. --- .../TelemetryContextActivityProcessorTests.cs | 502 +++++++++++++++ .../TelemetryContextLogProcessorTests.cs | 585 ++++++++++++++++++ .../TelemetryContextActivityProcessor.cs | 166 ++++- .../TelemetryContextLogProcessor.cs | 274 ++++++-- 4 files changed, 1484 insertions(+), 43 deletions(-) create mode 100644 BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryContextActivityProcessorTests.cs create mode 100644 BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryContextLogProcessorTests.cs diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryContextActivityProcessorTests.cs b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryContextActivityProcessorTests.cs new file mode 100644 index 0000000000..4a63157635 --- /dev/null +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryContextActivityProcessorTests.cs @@ -0,0 +1,502 @@ +namespace Microsoft.ApplicationInsights.Processors +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Internal; + using Xunit; + + public class TelemetryContextActivityProcessorTests : IDisposable + { + private readonly ActivitySource activitySource; + private readonly ActivityListener activityListener; + + public TelemetryContextActivityProcessorTests() + { + this.activitySource = new ActivitySource("Test.TelemetryContextActivityProcessor"); + this.activityListener = new ActivityListener + { + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + }; + ActivitySource.AddActivityListener(this.activityListener); + } + + public void Dispose() + { + this.activityListener.Dispose(); + this.activitySource.Dispose(); + } + + #region Basic tag application + + [Fact] + public void OnEnd_AppliesUserIdTag() + { + var context = new TelemetryContext(); + context.User.Id = "user-123"; + var processor = new TelemetryContextActivityProcessor(context); + + using var activity = this.CreateStoppedActivity(); + processor.OnEnd(activity); + + Assert.Equal("user-123", activity.GetTagItem(SemanticConventions.AttributeEnduserPseudoId)); + } + + [Fact] + public void OnEnd_AppliesAllContextTags() + { + var context = new TelemetryContext(); + context.User.Id = "user-123"; + context.User.AuthenticatedUserId = "auth-456"; + context.User.AccountId = "acct-789"; + context.Operation.Name = "GET /api"; + context.Operation.SyntheticSource = "bot"; + context.Location.Ip = "10.0.0.1"; + context.Session.Id = "session-1"; + context.Device.Id = "device-1"; + context.Device.Model = "Surface"; + context.Device.Type = "PC"; + context.Device.OperatingSystem = "Windows 11"; + + var processor = new TelemetryContextActivityProcessor(context); + using var activity = this.CreateStoppedActivity(); + processor.OnEnd(activity); + + Assert.Equal("user-123", activity.GetTagItem(SemanticConventions.AttributeEnduserPseudoId)); + Assert.Equal("auth-456", activity.GetTagItem(SemanticConventions.AttributeEnduserId)); + Assert.Equal("acct-789", activity.GetTagItem(SemanticConventions.AttributeMicrosoftUserAccountId)); + Assert.Equal("GET /api", activity.GetTagItem(SemanticConventions.AttributeMicrosoftOperationName)); + Assert.Equal("bot", activity.GetTagItem(SemanticConventions.AttributeMicrosoftSyntheticSource)); + Assert.Equal("10.0.0.1", activity.GetTagItem(SemanticConventions.AttributeMicrosoftClientIp)); + Assert.Equal("session-1", activity.GetTagItem(SemanticConventions.AttributeMicrosoftSessionId)); + Assert.Equal("device-1", activity.GetTagItem(SemanticConventions.AttributeAiDeviceId)); + Assert.Equal("Surface", activity.GetTagItem(SemanticConventions.AttributeAiDeviceModel)); + Assert.Equal("PC", activity.GetTagItem(SemanticConventions.AttributeAiDeviceType)); + Assert.Equal("Windows 11", activity.GetTagItem(SemanticConventions.AttributeAiDeviceOsVersion)); + } + + [Fact] + public void OnEnd_AppliesGlobalProperties() + { + var context = new TelemetryContext(); + context.GlobalProperties["env"] = "production"; + context.GlobalProperties["region"] = "westus2"; + + var processor = new TelemetryContextActivityProcessor(context); + using var activity = this.CreateStoppedActivity(); + processor.OnEnd(activity); + + Assert.Equal("production", activity.GetTagItem("env")); + Assert.Equal("westus2", activity.GetTagItem("region")); + } + + #endregion + + #region Skip-if-present semantics + + [Fact] + public void OnEnd_DoesNotOverwriteExistingTag() + { + var context = new TelemetryContext(); + context.User.Id = "context-user"; + + var processor = new TelemetryContextActivityProcessor(context); + using var activity = this.CreateStoppedActivity(); + activity.SetTag(SemanticConventions.AttributeEnduserPseudoId, "item-user"); + processor.OnEnd(activity); + + Assert.Equal("item-user", activity.GetTagItem(SemanticConventions.AttributeEnduserPseudoId)); + } + + [Fact] + public void OnEnd_DoesNotOverwriteExistingGlobalPropertyTag() + { + var context = new TelemetryContext(); + context.GlobalProperties["env"] = "production"; + + var processor = new TelemetryContextActivityProcessor(context); + using var activity = this.CreateStoppedActivity(); + activity.SetTag("env", "staging"); + processor.OnEnd(activity); + + Assert.Equal("staging", activity.GetTagItem("env")); + } + + #endregion + + #region Null/empty handling + + [Fact] + public void OnEnd_SkipsNullValues() + { + var context = new TelemetryContext(); + // Don't set any properties — all are null by default. + var processor = new TelemetryContextActivityProcessor(context); + using var activity = this.CreateStoppedActivity(); + processor.OnEnd(activity); + + Assert.Null(activity.GetTagItem(SemanticConventions.AttributeEnduserPseudoId)); + Assert.Null(activity.GetTagItem(SemanticConventions.AttributeEnduserId)); + Assert.Null(activity.GetTagItem(SemanticConventions.AttributeMicrosoftOperationName)); + } + + [Fact] + public void OnEnd_HandlesNullActivity() + { + var context = new TelemetryContext(); + var processor = new TelemetryContextActivityProcessor(context); + + // Should not throw. + processor.OnEnd(null); + } + + [Fact] + public void OnEnd_HandlesEmptyContext() + { + var context = new TelemetryContext(); + var processor = new TelemetryContextActivityProcessor(context); + using var activity = this.CreateStoppedActivity(); + + // Should not throw, and no tags should be added. + processor.OnEnd(activity); + + var tags = new List>(); + foreach (var tag in activity.Tags) + { + tags.Add(new KeyValuePair(tag.Key, tag.Value)); + } + + // Only the tags that the Activity already had (none from context). + Assert.Empty(tags); + } + + #endregion + + #region Warmup and snapshot freezing + + [Fact] + public void OnEnd_UsesSlowPathDuringWarmup() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + + var processor = new TelemetryContextActivityProcessor(context); + + // During warmup, each call should still correctly apply tags. + for (int i = 0; i < TelemetryContextActivityProcessor.WarmupCountThreshold - 1; i++) + { + using var activity = this.CreateStoppedActivity(); + processor.OnEnd(activity); + Assert.Equal("user-1", activity.GetTagItem(SemanticConventions.AttributeEnduserPseudoId)); + } + } + + [Fact] + public void OnEnd_FreezesSnapshotAfterWarmup() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + context.Device.Id = "device-1"; + + var processor = new TelemetryContextActivityProcessor(context); + ForceWarmupComplete(processor); + + // After warmup, snapshot should be frozen. + // Verify by mutating context — the frozen snapshot should still have original values. + context.User.Id = "user-CHANGED"; + + using var activity = this.CreateStoppedActivity(); + processor.OnEnd(activity); + + // The frozen snapshot captured "user-1", so even though context changed, + // the activity should get the original value. + Assert.Equal("user-1", activity.GetTagItem(SemanticConventions.AttributeEnduserPseudoId)); + Assert.Equal("device-1", activity.GetTagItem(SemanticConventions.AttributeAiDeviceId)); + } + + [Fact] + public void OnEnd_SnapshotOnlyContainsNonNullValues() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + // All other properties are null. + + var processor = new TelemetryContextActivityProcessor(context); + ForceWarmupComplete(processor); + + using var activity = this.CreateStoppedActivity(); + processor.OnEnd(activity); + + Assert.Equal("user-1", activity.GetTagItem(SemanticConventions.AttributeEnduserPseudoId)); + // Null properties should not appear as tags. + Assert.Null(activity.GetTagItem(SemanticConventions.AttributeAiDeviceId)); + Assert.Null(activity.GetTagItem(SemanticConventions.AttributeMicrosoftOperationName)); + } + + [Fact] + public void OnEnd_SnapshotIncludesGlobalProperties() + { + var context = new TelemetryContext(); + context.GlobalProperties["env"] = "prod"; + context.User.Id = "user-1"; + + var processor = new TelemetryContextActivityProcessor(context); + ForceWarmupComplete(processor); + + using var activity = this.CreateStoppedActivity(); + processor.OnEnd(activity); + + Assert.Equal("prod", activity.GetTagItem("env")); + Assert.Equal("user-1", activity.GetTagItem(SemanticConventions.AttributeEnduserPseudoId)); + } + + [Fact] + public void OnEnd_SnapshotRespectsSkipIfPresent() + { + var context = new TelemetryContext(); + context.User.Id = "context-user"; + + var processor = new TelemetryContextActivityProcessor(context); + ForceWarmupComplete(processor); + + using var activity = this.CreateStoppedActivity(); + activity.SetTag(SemanticConventions.AttributeEnduserPseudoId, "item-user"); + processor.OnEnd(activity); + + Assert.Equal("item-user", activity.GetTagItem(SemanticConventions.AttributeEnduserPseudoId)); + } + + [Fact] + public void WarmupDoesNotFreezeBeforeTimeThreshold() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + + var processor = new TelemetryContextActivityProcessor(context); + + // Run enough calls to pass the count threshold, but the time threshold + // should prevent freezing (we just created the processor). + for (int i = 0; i < TelemetryContextActivityProcessor.WarmupCountThreshold + 5; i++) + { + using var activity = this.CreateStoppedActivity(); + processor.OnEnd(activity); + } + + // The frozenTags field should still be null because <5s has elapsed. + var frozenTags = GetFrozenTags(processor); + Assert.Null(frozenTags); + } + + [Fact] + public void WarmupFreezesAfterBothThresholdsMet() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + + var processor = new TelemetryContextActivityProcessor(context); + ForceWarmupComplete(processor); + + var frozenTags = GetFrozenTags(processor); + Assert.NotNull(frozenTags); + Assert.Single(frozenTags); // Only user.Id was set. + } + + [Fact] + public void BuildSnapshot_NoDuplicateKeysWhenGlobalPropertiesOverlapContextFields() + { + var context = new TelemetryContext(); + context.User.Id = "context-user"; + context.GlobalProperties[SemanticConventions.AttributeEnduserPseudoId] = "global-user"; + context.GlobalProperties["extra"] = "value"; + + var processor = new TelemetryContextActivityProcessor(context); + ForceWarmupComplete(processor); + + var frozenTags = GetFrozenTags(processor); + Assert.NotNull(frozenTags); + + var keys = new HashSet(); + foreach (var kvp in frozenTags) + { + Assert.True(keys.Add(kvp.Key), $"Duplicate key in snapshot: '{kvp.Key}'"); + } + + // 2 entries: "enduser.pseudo.id" from GlobalProperties, "extra" from GlobalProperties. + // User.Id should NOT add a duplicate "enduser.pseudo.id". + Assert.Equal(2, frozenTags.Length); + } + + #endregion + + #region Thread safety + + [Fact] + public void OnEnd_IsThreadSafe() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + context.Device.Id = "device-1"; + + var processor = new TelemetryContextActivityProcessor(context); + ForceWarmupComplete(processor); + + // Run many concurrent OnEnd calls on the frozen snapshot path. + const int threadCount = 8; + const int iterationsPerThread = 1000; + var exceptions = new List(); + + var barrier = new Barrier(threadCount); + var threads = new Thread[threadCount]; + + for (int t = 0; t < threadCount; t++) + { + threads[t] = new Thread(() => + { + try + { + barrier.SignalAndWait(); + for (int i = 0; i < iterationsPerThread; i++) + { + using var activity = this.CreateStoppedActivity(); + processor.OnEnd(activity); + + Assert.Equal("user-1", activity.GetTagItem(SemanticConventions.AttributeEnduserPseudoId)); + Assert.Equal("device-1", activity.GetTagItem(SemanticConventions.AttributeAiDeviceId)); + } + } + catch (Exception ex) + { + lock (exceptions) + { + exceptions.Add(ex); + } + } + }); + threads[t].Start(); + } + + foreach (var thread in threads) + { + thread.Join(); + } + + Assert.Empty(exceptions); + } + + [Fact] + public void OnEnd_ConcurrentWarmupProducesValidSnapshot() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + context.Session.Id = "session-1"; + + var processor = new TelemetryContextActivityProcessor(context); + + // Backdate the timestamp so the time threshold is already met. + ForceTimestampToFarPast(processor); + + // Fire many threads concurrently to race through warmup and snapshot creation. + const int threadCount = 16; + var barrier = new Barrier(threadCount); + var threads = new Thread[threadCount]; + var exceptions = new List(); + + for (int t = 0; t < threadCount; t++) + { + threads[t] = new Thread(() => + { + try + { + barrier.SignalAndWait(); + for (int i = 0; i < 20; i++) + { + using var activity = this.CreateStoppedActivity(); + processor.OnEnd(activity); + Assert.Equal("user-1", activity.GetTagItem(SemanticConventions.AttributeEnduserPseudoId)); + } + } + catch (Exception ex) + { + lock (exceptions) + { + exceptions.Add(ex); + } + } + }); + threads[t].Start(); + } + + foreach (var thread in threads) + { + thread.Join(); + } + + Assert.Empty(exceptions); + + // After all threads finished, snapshot must exist and be valid. + var frozenTags = GetFrozenTags(processor); + Assert.NotNull(frozenTags); + Assert.Equal(2, frozenTags.Length); + } + + #endregion + + #region Helpers + + private Activity CreateStoppedActivity() + { + var activity = this.activitySource.StartActivity("TestOp"); + Assert.NotNull(activity); + activity.Stop(); + return activity; + } + + /// + /// Forces the processor past both warmup thresholds (count + time) by: + /// 1. Backdating the constructedTimestamp so the time threshold is met. + /// 2. Calling OnEnd enough times to trigger the count threshold. + /// + private void ForceWarmupComplete(TelemetryContextActivityProcessor processor) + { + ForceTimestampToFarPast(processor); + + for (int i = 0; i < TelemetryContextActivityProcessor.WarmupCountThreshold + 1; i++) + { + using var activity = this.CreateStoppedActivity(); + processor.OnEnd(activity); + } + } + + /// + /// Sets the constructedTimestamp to a value far in the past so the time threshold is immediately met. + /// + private static void ForceTimestampToFarPast(TelemetryContextActivityProcessor processor) + { + var field = typeof(TelemetryContextActivityProcessor) + .GetField("constructedTimestamp", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(field); + + // Set to a timestamp 10 seconds in the past. + long pastTimestamp = Stopwatch.GetTimestamp() - (Stopwatch.Frequency * 10); + field.SetValue(processor, pastTimestamp); + } + + /// + /// Reads the frozenTags field via reflection for test assertions. + /// + private static KeyValuePair[] GetFrozenTags(TelemetryContextActivityProcessor processor) + { + var field = typeof(TelemetryContextActivityProcessor) + .GetField("frozenTags", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(field); + return (KeyValuePair[]?)field.GetValue(processor); + } + + #endregion + } +} diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryContextLogProcessorTests.cs b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryContextLogProcessorTests.cs new file mode 100644 index 0000000000..591c528604 --- /dev/null +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryContextLogProcessorTests.cs @@ -0,0 +1,585 @@ +namespace Microsoft.ApplicationInsights.Processors +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Reflection; + using System.Threading; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Internal; + using OpenTelemetry.Logs; + using Xunit; + + public class TelemetryContextLogProcessorTests + { + #region Basic attribute application + + [Fact] + public void OnEnd_AppliesUserIdAttribute() + { + var context = new TelemetryContext(); + context.User.Id = "user-123"; + var processor = new TelemetryContextLogProcessor(context); + + var logRecord = CreateLogRecord(); + processor.OnEnd(logRecord); + + AssertAttributeValue("user-123", logRecord, SemanticConventions.AttributeEnduserPseudoId); + } + + [Fact] + public void OnEnd_AppliesAllContextAttributes() + { + var context = new TelemetryContext(); + context.User.Id = "user-123"; + context.User.AuthenticatedUserId = "auth-456"; + context.User.AccountId = "acct-789"; + context.Operation.Name = "GET /api"; + context.Operation.SyntheticSource = "bot"; + context.Location.Ip = "10.0.0.1"; + context.Session.Id = "session-1"; + context.Device.Id = "device-1"; + context.Device.Model = "Surface"; + context.Device.Type = "PC"; + context.Device.OperatingSystem = "Windows 11"; + + var processor = new TelemetryContextLogProcessor(context); + var logRecord = CreateLogRecord(); + processor.OnEnd(logRecord); + + AssertAttributeValue("user-123", logRecord, SemanticConventions.AttributeEnduserPseudoId); + AssertAttributeValue("auth-456", logRecord, SemanticConventions.AttributeEnduserId); + AssertAttributeValue("acct-789", logRecord, SemanticConventions.AttributeMicrosoftUserAccountId); + AssertAttributeValue("GET /api", logRecord, SemanticConventions.AttributeMicrosoftOperationName); + AssertAttributeValue("bot", logRecord, SemanticConventions.AttributeMicrosoftSyntheticSource); + AssertAttributeValue("10.0.0.1", logRecord, SemanticConventions.AttributeMicrosoftClientIp); + AssertAttributeValue("session-1", logRecord, SemanticConventions.AttributeMicrosoftSessionId); + AssertAttributeValue("device-1", logRecord, SemanticConventions.AttributeAiDeviceId); + AssertAttributeValue("Surface", logRecord, SemanticConventions.AttributeAiDeviceModel); + AssertAttributeValue("PC", logRecord, SemanticConventions.AttributeAiDeviceType); + AssertAttributeValue("Windows 11", logRecord, SemanticConventions.AttributeAiDeviceOsVersion); + } + + [Fact] + public void OnEnd_AppliesGlobalProperties() + { + var context = new TelemetryContext(); + context.GlobalProperties["env"] = "production"; + context.GlobalProperties["region"] = "westus2"; + + var processor = new TelemetryContextLogProcessor(context); + var logRecord = CreateLogRecord(); + processor.OnEnd(logRecord); + + AssertAttributeValue("production", logRecord, "env"); + AssertAttributeValue("westus2", logRecord, "region"); + } + + #endregion + + #region Skip-if-present semantics + + [Fact] + public void OnEnd_DoesNotOverwriteExistingAttribute() + { + var context = new TelemetryContext(); + context.User.Id = "context-user"; + + var processor = new TelemetryContextLogProcessor(context); + var logRecord = CreateLogRecordWithAttributes( + new KeyValuePair(SemanticConventions.AttributeEnduserPseudoId, "item-user")); + processor.OnEnd(logRecord); + + AssertAttributeValue("item-user", logRecord, SemanticConventions.AttributeEnduserPseudoId); + } + + [Fact] + public void OnEnd_DoesNotOverwriteExistingGlobalPropertyAttribute() + { + var context = new TelemetryContext(); + context.GlobalProperties["env"] = "production"; + + var processor = new TelemetryContextLogProcessor(context); + var logRecord = CreateLogRecordWithAttributes( + new KeyValuePair("env", "staging")); + processor.OnEnd(logRecord); + + AssertAttributeValue("staging", logRecord, "env"); + } + + [Fact] + public void OnEnd_PreservesExistingAttributesWhenMerging() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + + var processor = new TelemetryContextLogProcessor(context); + var logRecord = CreateLogRecordWithAttributes( + new KeyValuePair("custom-key", "custom-value")); + processor.OnEnd(logRecord); + + AssertAttributeValue("custom-value", logRecord, "custom-key"); + AssertAttributeValue("user-1", logRecord, SemanticConventions.AttributeEnduserPseudoId); + } + + #endregion + + #region Null/empty handling + + [Fact] + public void OnEnd_SkipsNullValues() + { + var context = new TelemetryContext(); + var processor = new TelemetryContextLogProcessor(context); + var logRecord = CreateLogRecord(); + processor.OnEnd(logRecord); + + // No attributes should be added since nothing is set. + Assert.True(logRecord.Attributes == null || logRecord.Attributes.Count == 0); + } + + [Fact] + public void OnEnd_HandlesNullLogRecord() + { + var context = new TelemetryContext(); + var processor = new TelemetryContextLogProcessor(context); + + // Should not throw. + processor.OnEnd(null); + } + + [Fact] + public void OnEnd_HandlesLogRecordWithNullAttributes() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + + var processor = new TelemetryContextLogProcessor(context); + var logRecord = CreateLogRecord(); + Assert.Null(logRecord.Attributes); + + processor.OnEnd(logRecord); + + AssertAttributeValue("user-1", logRecord, SemanticConventions.AttributeEnduserPseudoId); + } + + #endregion + + #region Warmup and snapshot freezing + + [Fact] + public void OnEnd_UsesSlowPathDuringWarmup() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + + var processor = new TelemetryContextLogProcessor(context); + + for (int i = 0; i < TelemetryContextLogProcessor.WarmupCountThreshold - 1; i++) + { + var logRecord = CreateLogRecord(); + processor.OnEnd(logRecord); + AssertAttributeValue("user-1", logRecord, SemanticConventions.AttributeEnduserPseudoId); + } + } + + [Fact] + public void OnEnd_FreezesSnapshotAfterWarmup() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + context.Device.Id = "device-1"; + + var processor = new TelemetryContextLogProcessor(context); + ForceWarmupComplete(processor); + + // Mutate context — frozen snapshot should retain original values. + context.User.Id = "user-CHANGED"; + + var logRecord = CreateLogRecord(); + processor.OnEnd(logRecord); + + AssertAttributeValue("user-1", logRecord, SemanticConventions.AttributeEnduserPseudoId); + AssertAttributeValue("device-1", logRecord, SemanticConventions.AttributeAiDeviceId); + } + + [Fact] + public void OnEnd_SnapshotOnlyContainsNonNullValues() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + + var processor = new TelemetryContextLogProcessor(context); + ForceWarmupComplete(processor); + + var frozenAttrs = GetFrozenAttributes(processor); + Assert.NotNull(frozenAttrs); + Assert.Single(frozenAttrs); + Assert.Equal(SemanticConventions.AttributeEnduserPseudoId, frozenAttrs[0].Key); + } + + [Fact] + public void OnEnd_SnapshotIncludesGlobalProperties() + { + var context = new TelemetryContext(); + context.GlobalProperties["env"] = "prod"; + context.User.Id = "user-1"; + + var processor = new TelemetryContextLogProcessor(context); + ForceWarmupComplete(processor); + + var logRecord = CreateLogRecord(); + processor.OnEnd(logRecord); + + AssertAttributeValue("prod", logRecord, "env"); + AssertAttributeValue("user-1", logRecord, SemanticConventions.AttributeEnduserPseudoId); + } + + [Fact] + public void OnEnd_SnapshotRespectsSkipIfPresent() + { + var context = new TelemetryContext(); + context.User.Id = "context-user"; + + var processor = new TelemetryContextLogProcessor(context); + ForceWarmupComplete(processor); + + var logRecord = CreateLogRecordWithAttributes( + new KeyValuePair(SemanticConventions.AttributeEnduserPseudoId, "item-user")); + processor.OnEnd(logRecord); + + AssertAttributeValue("item-user", logRecord, SemanticConventions.AttributeEnduserPseudoId); + } + + [Fact] + public void WarmupDoesNotFreezeBeforeTimeThreshold() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + + var processor = new TelemetryContextLogProcessor(context); + + for (int i = 0; i < TelemetryContextLogProcessor.WarmupCountThreshold + 5; i++) + { + var logRecord = CreateLogRecord(); + processor.OnEnd(logRecord); + } + + var frozenAttrs = GetFrozenAttributes(processor); + Assert.Null(frozenAttrs); + } + + [Fact] + public void WarmupFreezesAfterBothThresholdsMet() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + + var processor = new TelemetryContextLogProcessor(context); + ForceWarmupComplete(processor); + + var frozenAttrs = GetFrozenAttributes(processor); + Assert.NotNull(frozenAttrs); + Assert.Single(frozenAttrs); + } + + #endregion + + #region Allocation reduction verification + + [Fact] + public void OnEnd_FastPathDoesNotAllocateWhenNoAttributesToAdd() + { + var context = new TelemetryContext(); + // No properties set — snapshot will be empty array. + var processor = new TelemetryContextLogProcessor(context); + ForceWarmupComplete(processor); + + var frozenAttrs = GetFrozenAttributes(processor); + Assert.NotNull(frozenAttrs); + Assert.Empty(frozenAttrs); + + // Fast path with empty snapshot should not modify the log record. + var logRecord = CreateLogRecord(); + processor.OnEnd(logRecord); + + Assert.Null(logRecord.Attributes); + } + + [Fact] + public void OnEnd_FastPathDoesNotModifyAttributesWhenAllKeysConflict() + { + var context = new TelemetryContext(); + context.User.Id = "context-user"; + + var processor = new TelemetryContextLogProcessor(context); + ForceWarmupComplete(processor); + + var originalAttrs = new List> + { + new KeyValuePair(SemanticConventions.AttributeEnduserPseudoId, "existing-user"), + }; + var logRecord = CreateLogRecordWithAttributes(originalAttrs.ToArray()); + var attrsBefore = logRecord.Attributes; + processor.OnEnd(logRecord); + + // Attributes should not have been replaced since no new keys were added. + Assert.Same(attrsBefore, logRecord.Attributes); + } + + #endregion + + #region Thread safety + + [Fact] + public void OnEnd_IsThreadSafe() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + context.Device.Id = "device-1"; + + var processor = new TelemetryContextLogProcessor(context); + ForceWarmupComplete(processor); + + const int threadCount = 8; + const int iterationsPerThread = 1000; + var exceptions = new List(); + var barrier = new Barrier(threadCount); + var threads = new Thread[threadCount]; + + for (int t = 0; t < threadCount; t++) + { + threads[t] = new Thread(() => + { + try + { + barrier.SignalAndWait(); + for (int i = 0; i < iterationsPerThread; i++) + { + var logRecord = CreateLogRecord(); + processor.OnEnd(logRecord); + AssertAttributeValue("user-1", logRecord, SemanticConventions.AttributeEnduserPseudoId); + AssertAttributeValue("device-1", logRecord, SemanticConventions.AttributeAiDeviceId); + } + } + catch (Exception ex) + { + lock (exceptions) + { + exceptions.Add(ex); + } + } + }); + threads[t].Start(); + } + + foreach (var thread in threads) + { + thread.Join(); + } + + Assert.Empty(exceptions); + } + + [Fact] + public void OnEnd_ConcurrentWarmupProducesValidSnapshot() + { + var context = new TelemetryContext(); + context.User.Id = "user-1"; + context.Session.Id = "session-1"; + + var processor = new TelemetryContextLogProcessor(context); + ForceTimestampToFarPast(processor); + + const int threadCount = 16; + var barrier = new Barrier(threadCount); + var threads = new Thread[threadCount]; + var exceptions = new List(); + + for (int t = 0; t < threadCount; t++) + { + threads[t] = new Thread(() => + { + try + { + barrier.SignalAndWait(); + for (int i = 0; i < 20; i++) + { + var logRecord = CreateLogRecord(); + processor.OnEnd(logRecord); + AssertAttributeValue("user-1", logRecord, SemanticConventions.AttributeEnduserPseudoId); + } + } + catch (Exception ex) + { + lock (exceptions) + { + exceptions.Add(ex); + } + } + }); + threads[t].Start(); + } + + foreach (var thread in threads) + { + thread.Join(); + } + + Assert.Empty(exceptions); + + var frozenAttrs = GetFrozenAttributes(processor); + Assert.NotNull(frozenAttrs); + Assert.Equal(2, frozenAttrs.Length); + } + + #endregion + + #region GlobalProperties overlap with context fields + + [Fact] + public void OnEnd_SlowPath_NoDuplicateKeysWhenGlobalPropertiesOverlapContextFields() + { + var context = new TelemetryContext(); + context.User.Id = "context-user"; + context.Location.Ip = "1.1.1.1"; + context.GlobalProperties[SemanticConventions.AttributeEnduserPseudoId] = "global-user"; + context.GlobalProperties[SemanticConventions.AttributeMicrosoftClientIp] = "2.2.2.2"; + + var processor = new TelemetryContextLogProcessor(context); + var logRecord = CreateLogRecord(); + processor.OnEnd(logRecord); + + // GlobalProperties should win over explicit context fields (applied first). + // There must be no duplicate keys. + Assert.NotNull(logRecord.Attributes); + var keys = new HashSet(); + foreach (var attr in logRecord.Attributes) + { + Assert.True(keys.Add(attr.Key), $"Duplicate key found: '{attr.Key}'"); + } + + AssertAttributeValue("global-user", logRecord, SemanticConventions.AttributeEnduserPseudoId); + AssertAttributeValue("2.2.2.2", logRecord, SemanticConventions.AttributeMicrosoftClientIp); + } + + [Fact] + public void OnEnd_FastPath_NoDuplicateKeysWhenGlobalPropertiesOverlapContextFields() + { + var context = new TelemetryContext(); + context.User.Id = "context-user"; + context.Location.Ip = "1.1.1.1"; + context.GlobalProperties[SemanticConventions.AttributeEnduserPseudoId] = "global-user"; + context.GlobalProperties[SemanticConventions.AttributeMicrosoftClientIp] = "2.2.2.2"; + + var processor = new TelemetryContextLogProcessor(context); + ForceWarmupComplete(processor); + + var logRecord = CreateLogRecord(); + processor.OnEnd(logRecord); + + Assert.NotNull(logRecord.Attributes); + var keys = new HashSet(); + foreach (var attr in logRecord.Attributes) + { + Assert.True(keys.Add(attr.Key), $"Duplicate key found in snapshot: '{attr.Key}'"); + } + + // GlobalProperties win (they are added to the snapshot first). + AssertAttributeValue("global-user", logRecord, SemanticConventions.AttributeEnduserPseudoId); + AssertAttributeValue("2.2.2.2", logRecord, SemanticConventions.AttributeMicrosoftClientIp); + } + + [Fact] + public void BuildSnapshot_NoDuplicateKeysWhenGlobalPropertiesOverlapContextFields() + { + var context = new TelemetryContext(); + context.User.Id = "context-user"; + context.GlobalProperties[SemanticConventions.AttributeEnduserPseudoId] = "global-user"; + context.GlobalProperties["extra"] = "value"; + + var processor = new TelemetryContextLogProcessor(context); + ForceWarmupComplete(processor); + + var frozenAttrs = GetFrozenAttributes(processor); + Assert.NotNull(frozenAttrs); + + var keys = new HashSet(); + foreach (var kvp in frozenAttrs) + { + Assert.True(keys.Add(kvp.Key), $"Duplicate key in snapshot: '{kvp.Key}'"); + } + + // 2 entries: "enduser.pseudo.id" from GlobalProperties, "extra" from GlobalProperties. + // User.Id should NOT add a duplicate "enduser.pseudo.id". + Assert.Equal(2, frozenAttrs.Length); + } + + #endregion + + #region Helpers + + private static LogRecord CreateLogRecord() + { + // LogRecord has no public parameterless constructor; use reflection to invoke internal/private ctor. + return (LogRecord)Activator.CreateInstance( + typeof(LogRecord), + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, + binder: null, + args: null, + culture: null); + } + + private static LogRecord CreateLogRecordWithAttributes(params KeyValuePair[] attributes) + { + var logRecord = CreateLogRecord(); + logRecord.Attributes = new List>(attributes); + return logRecord; + } + + private static void AssertAttributeValue(object expected, LogRecord logRecord, string key) + { + Assert.NotNull(logRecord.Attributes); + foreach (var attr in logRecord.Attributes) + { + if (attr.Key == key) + { + Assert.Equal(expected, attr.Value); + return; + } + } + + Assert.Fail($"Attribute '{key}' not found in log record."); + } + + private static void ForceWarmupComplete(TelemetryContextLogProcessor processor) + { + ForceTimestampToFarPast(processor); + + for (int i = 0; i < TelemetryContextLogProcessor.WarmupCountThreshold + 1; i++) + { + var logRecord = CreateLogRecord(); + processor.OnEnd(logRecord); + } + } + + private static void ForceTimestampToFarPast(TelemetryContextLogProcessor processor) + { + var field = typeof(TelemetryContextLogProcessor) + .GetField("constructedTimestamp", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(field); + long pastTimestamp = Stopwatch.GetTimestamp() - (Stopwatch.Frequency * 10); + field.SetValue(processor, pastTimestamp); + } + + private static KeyValuePair[] GetFrozenAttributes(TelemetryContextLogProcessor processor) + { + var field = typeof(TelemetryContextLogProcessor) + .GetField("frozenAttributes", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(field); + return (KeyValuePair[])field.GetValue(processor); + } + + #endregion + } +} diff --git a/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs index 17b8abe63d..39ef431895 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs @@ -1,6 +1,9 @@ namespace Microsoft.ApplicationInsights.Processors { + using System.Collections.Generic; using System.Diagnostics; + using System.Runtime.CompilerServices; + using System.Threading; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Internal; using OpenTelemetry; @@ -11,9 +14,41 @@ namespace Microsoft.ApplicationInsights.Processors /// This ensures context tags are applied universally — to Track* calls, Start/Stop operations, /// and any OpenTelemetry API activities emitted by customer code. /// + /// + /// Performance optimization: after a warmup period ( calls), + /// context properties are frozen into a compact snapshot array. This avoids repeatedly + /// navigating the TelemetryContext object graph and skipping null/empty values on every call. + /// This relies on the documented pattern that customers set TelemetryClient.Context + /// properties once during initialization. + /// internal sealed class TelemetryContextActivityProcessor : BaseProcessor { + /// + /// Minimum number of OnEnd calls before the context snapshot can be frozen. + /// + internal const int WarmupCountThreshold = 10; + + /// + /// Minimum time (in milliseconds) after construction before the context snapshot + /// can be frozen. This guards against high-throughput apps where 10 activities + /// complete before async initialization (e.g., IHostedService, middleware) has + /// had a chance to set TelemetryContext properties. + /// + internal const long WarmupTimeThresholdMs = 5_000; + private readonly TelemetryContext context; + private readonly long constructedTimestamp; + + /// + /// Frozen snapshot of non-null context tags, built after warmup. + /// Once set, this array is immutable and read lock-free. + /// + private volatile KeyValuePair[] frozenTags; + + /// + /// Counter tracking the number of OnEnd calls during warmup. + /// + private int warmupCounter; /// /// Initializes a new instance of the class. @@ -22,6 +57,7 @@ internal sealed class TelemetryContextActivityProcessor : BaseProcessor @@ -38,6 +74,74 @@ public override void OnEnd(Activity activity) return; } + var snapshot = this.frozenTags; + if (snapshot != null) + { + // Fast path: apply pre-computed snapshot (only non-null values, no object graph navigation) + ApplySnapshot(activity, snapshot); + } + else + { + // Slow path: full context evaluation during warmup + this.SlowPathOnEnd(activity); + } + + base.OnEnd(activity); + } + + /// + /// Applies the frozen snapshot of context tags to the activity. + /// Only contains entries where the value was non-null/non-empty at snapshot time. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ApplySnapshot(Activity activity, KeyValuePair[] snapshot) + { + for (int i = 0; i < snapshot.Length; i++) + { + ref readonly var kvp = ref snapshot[i]; + if (activity.GetTagItem(kvp.Key) == null) + { + activity.SetTag(kvp.Key, kvp.Value); + } + } + } + + private static void AddIfNotEmpty(List> list, string key, string value) + { + if (!string.IsNullOrEmpty(value) && !ContainsKey(list, key)) + { + list.Add(new KeyValuePair(key, value)); + } + } + + private static bool ContainsKey(List> list, string key) + { + for (int i = 0; i < list.Count; i++) + { + if (list[i].Key == key) + { + return true; + } + } + + return false; + } + + private static void SetTagIfAbsent(Activity activity, string key, string value) + { + if (!string.IsNullOrEmpty(value) && activity.GetTagItem(key) == null) + { + activity.SetTag(key, value); + } + } + + /// + /// Full context evaluation path used during warmup. After + /// calls, builds and freezes the snapshot for all subsequent calls. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void SlowPathOnEnd(Activity activity) + { // Apply client-level GlobalProperties (lowest priority — will not overwrite existing tags) var globalProperties = this.context.GlobalPropertiesValue; if (globalProperties != null) @@ -60,15 +164,69 @@ public override void OnEnd(Activity activity) SetTagIfAbsent(activity, SemanticConventions.AttributeMicrosoftSyntheticSource, this.context.Operation?.SyntheticSource); SetTagIfAbsent(activity, SemanticConventions.AttributeMicrosoftUserAccountId, this.context.User?.AccountId); - base.OnEnd(activity); + // Freeze the snapshot once BOTH conditions are met: + // 1. At least WarmupCountThreshold calls have occurred. + // 2. At least WarmupTimeThresholdMs has elapsed since construction. + // The count gate ensures we don't snapshot on the very first call. + // The time gate ensures we don't snapshot before async init (e.g., + // IHostedService.StartAsync, middleware) has had time to set context. + int count = Interlocked.Increment(ref this.warmupCounter); + if (count >= WarmupCountThreshold && this.HasTimeThresholdElapsed()) + { + // CAS-style: only the thread that transitions from null builds the snapshot. + if (this.frozenTags == null) + { + Interlocked.CompareExchange(ref this.frozenTags, this.BuildSnapshot(), null); + } + } } - private static void SetTagIfAbsent(Activity activity, string key, string value) + /// + /// Builds an immutable snapshot array of all non-null/non-empty context tags. + /// Called once after warmup completes. + /// + private KeyValuePair[] BuildSnapshot() { - if (!string.IsNullOrEmpty(value) && activity.GetTagItem(key) == null) + var list = new List>(); + + var globalProperties = this.context.GlobalPropertiesValue; + if (globalProperties != null) { - activity.SetTag(key, value); + foreach (var kvp in globalProperties) + { + if (!string.IsNullOrEmpty(kvp.Value)) + { + list.Add(new KeyValuePair(kvp.Key, kvp.Value)); + } + } } + + AddIfNotEmpty(list, SemanticConventions.AttributeEnduserPseudoId, this.context.User?.Id); + AddIfNotEmpty(list, SemanticConventions.AttributeEnduserId, this.context.User?.AuthenticatedUserId); + AddIfNotEmpty(list, SemanticConventions.AttributeMicrosoftOperationName, this.context.Operation?.Name); + AddIfNotEmpty(list, SemanticConventions.AttributeMicrosoftClientIp, this.context.Location?.Ip); + AddIfNotEmpty(list, SemanticConventions.AttributeMicrosoftSessionId, this.context.Session?.Id); + AddIfNotEmpty(list, SemanticConventions.AttributeAiDeviceId, this.context.Device?.Id); + AddIfNotEmpty(list, SemanticConventions.AttributeAiDeviceModel, this.context.Device?.Model); + AddIfNotEmpty(list, SemanticConventions.AttributeAiDeviceType, this.context.Device?.Type); + AddIfNotEmpty(list, SemanticConventions.AttributeAiDeviceOsVersion, this.context.Device?.OperatingSystem); + AddIfNotEmpty(list, SemanticConventions.AttributeMicrosoftSyntheticSource, this.context.Operation?.SyntheticSource); + AddIfNotEmpty(list, SemanticConventions.AttributeMicrosoftUserAccountId, this.context.User?.AccountId); + + return list.ToArray(); + } + + /// + /// Returns true if at least milliseconds + /// have elapsed since this processor was constructed. + /// Uses for monotonic, allocation-free timing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool HasTimeThresholdElapsed() + { + long elapsedTicks = Stopwatch.GetTimestamp() - this.constructedTimestamp; + long elapsedMs = (elapsedTicks * 1000) / Stopwatch.Frequency; + return elapsedMs >= WarmupTimeThresholdMs; } } } diff --git a/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs index 9a91399dce..9db617dbd7 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs @@ -1,6 +1,9 @@ namespace Microsoft.ApplicationInsights.Processors { using System.Collections.Generic; + using System.Diagnostics; + using System.Runtime.CompilerServices; + using System.Threading; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Internal; using OpenTelemetry; @@ -12,9 +15,41 @@ namespace Microsoft.ApplicationInsights.Processors /// This ensures context attributes are applied universally — to Track* calls /// and any calls from customer code. /// + /// + /// Performance optimization: after a warmup period ( calls), + /// context properties are frozen into a compact snapshot array. This eliminates per-call + /// allocations of and intermediate buffers, + /// and avoids repeatedly navigating the object graph. + /// This relies on the documented pattern that customers set TelemetryClient.Context + /// properties once during initialization. + /// internal sealed class TelemetryContextLogProcessor : BaseProcessor { + /// + /// Minimum number of OnEnd calls before the context snapshot can be frozen. + /// + internal const int WarmupCountThreshold = 10; + + /// + /// Minimum time (in milliseconds) after construction before the context snapshot + /// can be frozen. This guards against high-throughput apps where activities + /// complete before async initialization has had a chance to set TelemetryContext properties. + /// + internal const long WarmupTimeThresholdMs = 5_000; + private readonly TelemetryContext context; + private readonly long constructedTimestamp; + + /// + /// Frozen snapshot of non-null context attributes, built after warmup. + /// Once set, this array is immutable and read lock-free. + /// + private volatile KeyValuePair[] frozenAttributes; + + /// + /// Counter tracking the number of OnEnd calls during warmup. + /// + private int warmupCounter; /// /// Initializes a new instance of the class. @@ -23,6 +58,7 @@ internal sealed class TelemetryContextLogProcessor : BaseProcessor public TelemetryContextLogProcessor(TelemetryContext context) { this.context = context; + this.constructedTimestamp = Stopwatch.GetTimestamp(); } /// @@ -40,74 +76,234 @@ public override void OnEnd(LogRecord logRecord) return; } - // Collect existing attribute keys to check for presence - var existingKeys = new HashSet(); - if (logRecord.Attributes != null) + var snapshot = this.frozenAttributes; + if (snapshot != null) { - foreach (var attr in logRecord.Attributes) - { - existingKeys.Add(attr.Key); - } + // Fast path: apply pre-computed snapshot (no HashSet, no intermediate List, no object graph) + ApplySnapshot(logRecord, snapshot); + } + else + { + // Slow path: full context evaluation during warmup + this.SlowPathOnEnd(logRecord); } - // Apply client-level GlobalProperties (lowest priority — will not overwrite existing keys) - var contextAttributes = new List>(); - var globalProperties = this.context.GlobalPropertiesValue; - if (globalProperties != null) + base.OnEnd(logRecord); + } + + /// + /// Fast path: merges the frozen snapshot attributes into the log record, + /// skipping any keys already present. + /// Zero-allocation when the log record has no pre-existing attributes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ApplySnapshot(LogRecord logRecord, KeyValuePair[] snapshot) + { + if (snapshot.Length == 0) { - foreach (var kvp in globalProperties) + return; + } + + var existing = logRecord.Attributes; + int existingCount = existing?.Count ?? 0; + + if (existingCount == 0) + { + // Most common case: no pre-existing attributes. + // Assign the snapshot array directly — T[] implements IReadOnlyList. + // Zero allocation. + logRecord.Attributes = snapshot; + return; + } + + // Count how many snapshot keys are NOT already present, to avoid + // allocating a merged list when all keys conflict. + int newCount = 0; + for (int i = 0; i < snapshot.Length; i++) + { + if (!ContainsKey(existing, snapshot[i].Key)) { - AddIfAbsent(contextAttributes, existingKeys, kvp.Key, kvp.Value); + newCount++; } } - // Build list of structured context attributes to add (only those not already present) - AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeEnduserPseudoId, this.context.User?.Id); - AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeEnduserId, this.context.User?.AuthenticatedUserId); - AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeMicrosoftOperationName, this.context.Operation?.Name); - AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeMicrosoftClientIp, this.context.Location?.Ip); - AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeMicrosoftSessionId, this.context.Session?.Id); - AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeAiDeviceId, this.context.Device?.Id); - AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeAiDeviceModel, this.context.Device?.Model); - AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeAiDeviceType, this.context.Device?.Type); - AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeAiDeviceOsVersion, this.context.Device?.OperatingSystem); - AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeMicrosoftSyntheticSource, this.context.Operation?.SyntheticSource); - AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeMicrosoftUserAccountId, this.context.User?.AccountId); - - if (contextAttributes.Count == 0) + if (newCount == 0) { - base.OnEnd(logRecord); return; } - // Merge original attributes with context attributes into a new list - var mergedAttributes = new List>( - (logRecord.Attributes?.Count ?? 0) + contextAttributes.Count); + // Only allocate when we actually have new attributes to merge. + var merged = new List>(existingCount + newCount); + foreach (var attr in existing) + { + merged.Add(attr); + } - if (logRecord.Attributes != null) + for (int i = 0; i < snapshot.Length; i++) { - foreach (var attr in logRecord.Attributes) + ref readonly var kvp = ref snapshot[i]; + if (!ContainsKey(existing, kvp.Key)) { - mergedAttributes.Add(attr); + merged.Add(kvp); } } - mergedAttributes.AddRange(contextAttributes); - logRecord.Attributes = mergedAttributes; + logRecord.Attributes = merged; + } + + /// + /// Checks whether the attribute collection contains the specified key. + /// Uses linear scan — attribute lists are typically small (<20 items). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ContainsKey(IReadOnlyList> attributes, string key) + { + if (attributes == null || attributes.Count == 0) + { + return false; + } + + for (int i = 0; i < attributes.Count; i++) + { + if (attributes[i].Key == key) + { + return true; + } + } - base.OnEnd(logRecord); + return false; } private static void AddIfAbsent( List> contextAttributes, - HashSet existingKeys, + IReadOnlyList> existingAttributes, string key, string value) { - if (!string.IsNullOrEmpty(value) && existingKeys.Add(key)) + if (!string.IsNullOrEmpty(value) + && !ContainsKey(existingAttributes, key) + && !ContainsKey(contextAttributes, key)) { contextAttributes.Add(new KeyValuePair(key, value)); } } + + private static void AddIfNotEmpty(List> list, string key, string value) + { + if (!string.IsNullOrEmpty(value) && !ContainsKey(list, key)) + { + list.Add(new KeyValuePair(key, value)); + } + } + + /// + /// Full context evaluation path used during warmup. After both thresholds are met, + /// builds and freezes the snapshot for all subsequent calls. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void SlowPathOnEnd(LogRecord logRecord) + { + var existing = logRecord.Attributes; + + // Build list of context attributes to add (only those not already present). + // No HashSet needed — linear scan on the small attributes list is faster and allocation-free. + var contextAttributes = new List>(); + var globalProperties = this.context.GlobalPropertiesValue; + if (globalProperties != null) + { + foreach (var kvp in globalProperties) + { + AddIfAbsent(contextAttributes, existing, kvp.Key, kvp.Value); + } + } + + AddIfAbsent(contextAttributes, existing, SemanticConventions.AttributeEnduserPseudoId, this.context.User?.Id); + AddIfAbsent(contextAttributes, existing, SemanticConventions.AttributeEnduserId, this.context.User?.AuthenticatedUserId); + AddIfAbsent(contextAttributes, existing, SemanticConventions.AttributeMicrosoftOperationName, this.context.Operation?.Name); + AddIfAbsent(contextAttributes, existing, SemanticConventions.AttributeMicrosoftClientIp, this.context.Location?.Ip); + AddIfAbsent(contextAttributes, existing, SemanticConventions.AttributeMicrosoftSessionId, this.context.Session?.Id); + AddIfAbsent(contextAttributes, existing, SemanticConventions.AttributeAiDeviceId, this.context.Device?.Id); + AddIfAbsent(contextAttributes, existing, SemanticConventions.AttributeAiDeviceModel, this.context.Device?.Model); + AddIfAbsent(contextAttributes, existing, SemanticConventions.AttributeAiDeviceType, this.context.Device?.Type); + AddIfAbsent(contextAttributes, existing, SemanticConventions.AttributeAiDeviceOsVersion, this.context.Device?.OperatingSystem); + AddIfAbsent(contextAttributes, existing, SemanticConventions.AttributeMicrosoftSyntheticSource, this.context.Operation?.SyntheticSource); + AddIfAbsent(contextAttributes, existing, SemanticConventions.AttributeMicrosoftUserAccountId, this.context.User?.AccountId); + + if (contextAttributes.Count > 0) + { + int existingCount = existing?.Count ?? 0; + var merged = new List>(existingCount + contextAttributes.Count); + + if (existing != null) + { + foreach (var attr in existing) + { + merged.Add(attr); + } + } + + merged.AddRange(contextAttributes); + logRecord.Attributes = merged; + } + + // Freeze the snapshot once BOTH conditions are met: + // 1. At least WarmupCountThreshold calls have occurred. + // 2. At least WarmupTimeThresholdMs has elapsed since construction. + int count = Interlocked.Increment(ref this.warmupCounter); + if (count >= WarmupCountThreshold && this.HasTimeThresholdElapsed()) + { + if (this.frozenAttributes == null) + { + Interlocked.CompareExchange(ref this.frozenAttributes, this.BuildSnapshot(), null); + } + } + } + + /// + /// Builds an immutable snapshot array of all non-null/non-empty context attributes. + /// Called once after warmup completes. + /// + private KeyValuePair[] BuildSnapshot() + { + var list = new List>(); + + var globalProperties = this.context.GlobalPropertiesValue; + if (globalProperties != null) + { + foreach (var kvp in globalProperties) + { + if (!string.IsNullOrEmpty(kvp.Value)) + { + list.Add(new KeyValuePair(kvp.Key, kvp.Value)); + } + } + } + + AddIfNotEmpty(list, SemanticConventions.AttributeEnduserPseudoId, this.context.User?.Id); + AddIfNotEmpty(list, SemanticConventions.AttributeEnduserId, this.context.User?.AuthenticatedUserId); + AddIfNotEmpty(list, SemanticConventions.AttributeMicrosoftOperationName, this.context.Operation?.Name); + AddIfNotEmpty(list, SemanticConventions.AttributeMicrosoftClientIp, this.context.Location?.Ip); + AddIfNotEmpty(list, SemanticConventions.AttributeMicrosoftSessionId, this.context.Session?.Id); + AddIfNotEmpty(list, SemanticConventions.AttributeAiDeviceId, this.context.Device?.Id); + AddIfNotEmpty(list, SemanticConventions.AttributeAiDeviceModel, this.context.Device?.Model); + AddIfNotEmpty(list, SemanticConventions.AttributeAiDeviceType, this.context.Device?.Type); + AddIfNotEmpty(list, SemanticConventions.AttributeAiDeviceOsVersion, this.context.Device?.OperatingSystem); + AddIfNotEmpty(list, SemanticConventions.AttributeMicrosoftSyntheticSource, this.context.Operation?.SyntheticSource); + AddIfNotEmpty(list, SemanticConventions.AttributeMicrosoftUserAccountId, this.context.User?.AccountId); + + return list.ToArray(); + } + + /// + /// Returns true if at least milliseconds + /// have elapsed since this processor was constructed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool HasTimeThresholdElapsed() + { + long elapsedTicks = Stopwatch.GetTimestamp() - this.constructedTimestamp; + long elapsedMs = (elapsedTicks * 1000) / Stopwatch.Frequency; + return elapsedMs >= WarmupTimeThresholdMs; + } } } From 696acc82edab05e5218b501d24adbebd8904f849 Mon Sep 17 00:00:00 2001 From: Rajkumar Rangaraj Date: Fri, 20 Mar 2026 13:45:06 -0700 Subject: [PATCH 10/15] Add TelemetryContext processors for Activity and Logger in ApplicationInsightsExtensions --- .../ApplicationInsightsExtensions.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/NETCORE/src/Microsoft.ApplicationInsights.WorkerService/ApplicationInsightsExtensions.cs b/NETCORE/src/Microsoft.ApplicationInsights.WorkerService/ApplicationInsightsExtensions.cs index 805c95b147..34a3c0506b 100644 --- a/NETCORE/src/Microsoft.ApplicationInsights.WorkerService/ApplicationInsightsExtensions.cs +++ b/NETCORE/src/Microsoft.ApplicationInsights.WorkerService/ApplicationInsightsExtensions.cs @@ -9,12 +9,14 @@ using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing; using Microsoft.ApplicationInsights.Internal; + using Microsoft.ApplicationInsights.Processors; using Microsoft.ApplicationInsights.Shared.Vendoring.OpenTelemetry.Resources; using Microsoft.ApplicationInsights.WorkerService; using Microsoft.ApplicationInsights.WorkerService.Implementation.Tracing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using OpenTelemetry; + using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -152,11 +154,19 @@ internal static IOpenTelemetryBuilder UseApplicationInsightsTelemetry(this IOpen return true; }) - .AddProcessor()); + .AddProcessor() + .AddProcessor(sp => + new TelemetryContextActivityProcessor(sp.GetRequiredService().Context))); // Register ActivityFilterProcessor in DI builder.Services.AddSingleton(); + builder.Services.ConfigureOpenTelemetryLoggerProvider((sp, loggerBuilder) => + { + loggerBuilder.AddProcessor( + new TelemetryContextLogProcessor(sp.GetRequiredService().Context)); + }); + builder.WithMetrics(b => b.AddHttpClientMetrics()); builder.Services.AddOptions() From 2985a4104bc0156ebc0fa4b5b687395f08a9f584 Mon Sep 17 00:00:00 2001 From: Harsimar Kaur Date: Fri, 20 Mar 2026 14:54:29 -0700 Subject: [PATCH 11/15] fix user agent --- .../Stable/PublicAPI.Unshipped.txt | 2 ++ .../TelemetryClientContextTagsTest.cs | 14 ++++++++++++-- .../TelemetryClientTest.cs | 12 ++++++------ .../Extensibility/Implementation/UserContext.cs | 2 +- .../TelemetryContextActivityProcessor.cs | 1 + .../Processors/TelemetryContextLogProcessor.cs | 1 + .../TelemetryClient.cs | 10 ++++++++++ examples/AspNetCoreWebApp/appsettings.json | 2 +- 8 files changed, 34 insertions(+), 10 deletions(-) diff --git a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt index 546aede6f2..bcb95412f4 100644 --- a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt @@ -16,6 +16,8 @@ Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext.Id.get Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext.Id.set -> void Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.AccountId.get -> string Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.AccountId.set -> void +Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.UserAgent.get -> string +Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.UserAgent.set -> void Microsoft.ApplicationInsights.DataContracts.TelemetryContext.Component.get -> Microsoft.ApplicationInsights.Extensibility.Implementation.ComponentContext Microsoft.ApplicationInsights.Extensibility.Implementation.ComponentContext Microsoft.ApplicationInsights.Extensibility.Implementation.ComponentContext.Version.get -> string diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs index c140da802b..547cbde760 100644 --- a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientContextTagsTest.cs @@ -54,6 +54,7 @@ public void TrackEvent_IncludesAllClientContextTags() this.telemetryClient.Context.User.Id = "evt-user"; this.telemetryClient.Context.User.AuthenticatedUserId = "evt-auth"; this.telemetryClient.Context.User.AccountId = "evt-acct"; + this.telemetryClient.Context.User.UserAgent = "evt-agent"; this.telemetryClient.Context.Operation.Name = "EvtOp"; this.telemetryClient.Context.Operation.SyntheticSource = "evt-bot"; this.telemetryClient.Context.Location.Ip = "10.0.0.2"; @@ -83,6 +84,7 @@ public void TrackEvent_IncludesAllClientContextTags() Assert.Equal("Android 14", attributes["ai.device.osVersion"]); Assert.Equal("evt-bot", attributes["microsoft.synthetic_source"]); Assert.Equal("evt-acct", attributes["microsoft.user.account_id"]); + Assert.Equal("evt-agent", attributes["user_agent.original"]); } [Fact] @@ -106,6 +108,7 @@ public void TrackTrace_IncludesAllClientContextTags() this.telemetryClient.Context.User.Id = "trc-user"; this.telemetryClient.Context.User.AuthenticatedUserId = "trc-auth"; this.telemetryClient.Context.User.AccountId = "trc-acct"; + this.telemetryClient.Context.User.UserAgent = "trc-agent"; this.telemetryClient.Context.Operation.Name = "TrcOp"; this.telemetryClient.Context.Operation.SyntheticSource = "trc-bot"; this.telemetryClient.Context.Location.Ip = "10.0.0.3"; @@ -132,6 +135,7 @@ public void TrackTrace_IncludesAllClientContextTags() Assert.Equal("Windows 10", attributes["ai.device.osVersion"]); Assert.Equal("trc-bot", attributes["microsoft.synthetic_source"]); Assert.Equal("trc-acct", attributes["microsoft.user.account_id"]); + Assert.Equal("trc-agent", attributes["user_agent.original"]); } [Fact] @@ -140,6 +144,7 @@ public void TrackException_IncludesAllClientContextTags() this.telemetryClient.Context.User.Id = "exc-user"; this.telemetryClient.Context.User.AuthenticatedUserId = "exc-auth"; this.telemetryClient.Context.User.AccountId = "exc-acct"; + this.telemetryClient.Context.User.UserAgent = "exc-agent"; this.telemetryClient.Context.Operation.Name = "ExcOp"; this.telemetryClient.Context.Operation.SyntheticSource = "exc-bot"; this.telemetryClient.Context.Location.Ip = "172.16.0.1"; @@ -166,6 +171,7 @@ public void TrackException_IncludesAllClientContextTags() Assert.Equal("Linux 6.1", attributes["ai.device.osVersion"]); Assert.Equal("exc-bot", attributes["microsoft.synthetic_source"]); Assert.Equal("exc-acct", attributes["microsoft.user.account_id"]); + Assert.Equal("exc-agent", attributes["user_agent.original"]); } [Fact] @@ -340,6 +346,7 @@ public void TrackRequest_IncludesAllClientContextTags() this.telemetryClient.Context.User.Id = "req-user"; this.telemetryClient.Context.User.AuthenticatedUserId = "req-auth"; this.telemetryClient.Context.User.AccountId = "req-acct"; + this.telemetryClient.Context.User.UserAgent = "req-agent"; this.telemetryClient.Context.Location.Ip = "10.0.0.1"; this.telemetryClient.Context.Session.Id = "req-session"; this.telemetryClient.Context.Device.Id = "req-device"; @@ -364,6 +371,7 @@ public void TrackRequest_IncludesAllClientContextTags() Assert.True(activity.Tags.Any(t => t.Key == "ai.device.osVersion" && t.Value == "macOS 14")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.synthetic_source" && t.Value == "test-runner")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.user.account_id" && t.Value == "req-acct")); + Assert.True(activity.Tags.Any(t => t.Key == "user_agent.original" && t.Value == "req-agent")); } [Fact] @@ -372,6 +380,7 @@ public void TrackDependency_IncludesAllClientContextTags() this.telemetryClient.Context.User.Id = "dep-user"; this.telemetryClient.Context.User.AuthenticatedUserId = "dep-auth"; this.telemetryClient.Context.User.AccountId = "dep-acct"; + this.telemetryClient.Context.User.UserAgent = "dep-agent"; this.telemetryClient.Context.Operation.Name = "ParentOp"; this.telemetryClient.Context.Operation.SyntheticSource = "dep-bot"; this.telemetryClient.Context.Location.Ip = "10.0.0.4"; @@ -404,6 +413,7 @@ public void TrackDependency_IncludesAllClientContextTags() Assert.True(activity.Tags.Any(t => t.Key == "ai.device.osVersion" && t.Value == "watchOS 10")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.synthetic_source" && t.Value == "dep-bot")); Assert.True(activity.Tags.Any(t => t.Key == "microsoft.user.account_id" && t.Value == "dep-acct")); + Assert.True(activity.Tags.Any(t => t.Key == "user_agent.original" && t.Value == "dep-agent")); } #endregion @@ -677,6 +687,7 @@ public void TrackEvent_NoContextSet_EmitsWithoutError() Assert.False(attributes.ContainsKey("ai.device.type")); Assert.False(attributes.ContainsKey("microsoft.synthetic_source")); Assert.False(attributes.ContainsKey("microsoft.user.account_id")); + Assert.False(attributes.ContainsKey("user_agent.original")); } [Fact] @@ -711,8 +722,7 @@ public void TrackRequest_NoContextSet_EmitsWithoutContextTags() Assert.False(activity.Tags.Any(t => t.Key == "ai.device.type")); Assert.False(activity.Tags.Any(t => t.Key == "microsoft.synthetic_source")); Assert.False(activity.Tags.Any(t => t.Key == "microsoft.user.account_id")); + Assert.False(activity.Tags.Any(t => t.Key == "user_agent.original")); } - - #endregion } } diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientTest.cs b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientTest.cs index cf184449fe..9972d71cf1 100644 --- a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientTest.cs +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientTest.cs @@ -2084,7 +2084,7 @@ public void TrackRequest_UserAgent_MapsToUserAgentOriginal() } [Fact] - public void TrackDependency_UserAgent_DoesNotMapToUserAgentOriginal() + public void TrackDependency_UserAgent_MapsToUserAgentOriginal() { // Arrange var dependency = new DependencyTelemetry @@ -2100,14 +2100,14 @@ public void TrackDependency_UserAgent_DoesNotMapToUserAgentOriginal() this.telemetryClient.TrackDependency(dependency); this.telemetryClient.Flush(); - // Assert - UserAgent should NOT be mapped for non-request telemetry + // Assert - UserAgent is mapped for all activity-based telemetry Assert.Equal(1, this.activityItems.Count); var activity = this.activityItems[0]; - Assert.False(activity.Tags.Any(t => t.Key == "user_agent.original")); + Assert.True(activity.Tags.Any(t => t.Key == "user_agent.original" && t.Value == "TestAgent/1.0")); } [Fact] - public void TrackEvent_UserAgent_DoesNotMapToUserAgentOriginal() + public void TrackEvent_UserAgent_MapsToUserAgentOriginal() { // Arrange var eventTelemetry = new EventTelemetry("TestEvent"); @@ -2117,11 +2117,11 @@ public void TrackEvent_UserAgent_DoesNotMapToUserAgentOriginal() this.telemetryClient.TrackEvent(eventTelemetry); this.telemetryClient.Flush(); - // Assert - UserAgent should NOT be mapped for events + // Assert - UserAgent is mapped for all telemetry types var logRecord = this.logItems.FirstOrDefault(l => l.Attributes != null && l.Attributes.Any(a => a.Key == "microsoft.custom_event.name")); Assert.NotNull(logRecord); var attributes = logRecord.Attributes.ToDictionary(a => a.Key, a => a.Value?.ToString()); - Assert.False(attributes.ContainsKey("user_agent.original")); + Assert.Equal("TestAgent/2.0", attributes["user_agent.original"]); } [Fact] diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs index bce14d4362..89945db7e6 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/UserContext.cs @@ -48,7 +48,7 @@ public string AccountId /// /// Gets or sets the UserAgent of an application-defined account associated with the user. /// - internal string UserAgent + public string UserAgent { get { return string.IsNullOrEmpty(this.userAgent) ? null : this.userAgent; } set { this.userAgent = value; } diff --git a/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs index 17b8abe63d..69e8c63c94 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextActivityProcessor.cs @@ -59,6 +59,7 @@ public override void OnEnd(Activity activity) SetTagIfAbsent(activity, SemanticConventions.AttributeAiDeviceOsVersion, this.context.Device?.OperatingSystem); SetTagIfAbsent(activity, SemanticConventions.AttributeMicrosoftSyntheticSource, this.context.Operation?.SyntheticSource); SetTagIfAbsent(activity, SemanticConventions.AttributeMicrosoftUserAccountId, this.context.User?.AccountId); + SetTagIfAbsent(activity, SemanticConventions.AttributeUserAgentOriginal, this.context.User?.UserAgent); base.OnEnd(activity); } diff --git a/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs index 9a91399dce..3c60d81f54 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Processors/TelemetryContextLogProcessor.cs @@ -73,6 +73,7 @@ public override void OnEnd(LogRecord logRecord) AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeAiDeviceOsVersion, this.context.Device?.OperatingSystem); AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeMicrosoftSyntheticSource, this.context.Operation?.SyntheticSource); AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeMicrosoftUserAccountId, this.context.User?.AccountId); + AddIfAbsent(contextAttributes, existingKeys, SemanticConventions.AttributeUserAgentOriginal, this.context.User?.UserAgent); if (contextAttributes.Count == 0) { diff --git a/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs b/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs index 37b410c365..19331f341b 100644 --- a/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs +++ b/BASE/src/Microsoft.ApplicationInsights/TelemetryClient.cs @@ -1398,6 +1398,11 @@ private static void ApplyContextToActivity(TelemetryContext context, Activity ac { activity.SetTag(SemanticConventions.AttributeMicrosoftUserAccountId, context.User.AccountId); } + + if (!string.IsNullOrEmpty(context.User?.UserAgent)) + { + activity.SetTag(SemanticConventions.AttributeUserAgentOriginal, context.User.UserAgent); + } } /// @@ -1466,6 +1471,11 @@ private static void ApplyContextToProperties(TelemetryContext context, IDictiona { properties[SemanticConventions.AttributeMicrosoftUserAccountId] = context.User.AccountId; } + + if (!string.IsNullOrEmpty(context.User?.UserAgent)) + { + properties[SemanticConventions.AttributeUserAgentOriginal] = context.User.UserAgent; + } } private readonly struct DictionaryLogState : IReadOnlyList> diff --git a/examples/AspNetCoreWebApp/appsettings.json b/examples/AspNetCoreWebApp/appsettings.json index 90798873f3..4006986711 100644 --- a/examples/AspNetCoreWebApp/appsettings.json +++ b/examples/AspNetCoreWebApp/appsettings.json @@ -8,6 +8,6 @@ }, "AllowedHosts": "*", "ApplicationInsights": { - "ConnectionString": "InstrumentationKey=11111111-2222-3333-4444-555555555555;IngestionEndpoint=http://testendpoint" + "ConnectionString": "" } } From 956a58156b6a574acf261ec7d1ee3b757b293cfc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:45:12 +0000 Subject: [PATCH 12/15] Remove Cloud and Component context settings from HomeController example Agent-Logs-Url: https://github.com/microsoft/ApplicationInsights-dotnet/sessions/b9572329-ebd7-4426-a448-54a8cc4fd6b9 Co-authored-by: harsimar <19897860+harsimar@users.noreply.github.com> --- examples/AspNetCoreWebApp/Controllers/HomeController.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/AspNetCoreWebApp/Controllers/HomeController.cs b/examples/AspNetCoreWebApp/Controllers/HomeController.cs index ab7d21cbdb..e933fc45f7 100644 --- a/examples/AspNetCoreWebApp/Controllers/HomeController.cs +++ b/examples/AspNetCoreWebApp/Controllers/HomeController.cs @@ -21,9 +21,6 @@ public HomeController(ILogger logger, TelemetryClient telemetryC this._logger = logger; this._telemetryClient = telemetryClient; telemetryClient.Context.User.Id = "TestUserId"; - telemetryClient.Context.Cloud.RoleName = "TestRoleName"; - telemetryClient.Context.Cloud.RoleInstance = "TestRoleInstance"; - telemetryClient.Context.Component.Version = "2.0.0"; telemetryClient.Context.User.UserAgent = "curl/8.0"; // In a real app, you wouldn't need the TelemetryConfiguration here. From 384a7c35d54a71a8a7e48f71a472356fd6ada727 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:48:25 +0000 Subject: [PATCH 13/15] Replace real connection string with dummy value; remove Setting Context Properties section from README Agent-Logs-Url: https://github.com/microsoft/ApplicationInsights-dotnet/sessions/f704c8d6-9288-49bd-a0f4-8c6a7b4cbbd9 Co-authored-by: harsimar <19897860+harsimar@users.noreply.github.com> --- BASE/README.md | 60 ----------------------------- examples/BasicConsoleApp/Program.cs | 2 +- 2 files changed, 1 insertion(+), 61 deletions(-) diff --git a/BASE/README.md b/BASE/README.md index 631cd8b5c2..b3ded39ee1 100644 --- a/BASE/README.md +++ b/BASE/README.md @@ -418,66 +418,6 @@ Note: Setting an OpenTelemetry Sampler via `builder.SetSampler()` is currently u -### Setting Context Properties - -In version 3.x, the following properties remain publicly settable on telemetry items: - -**Available Context Properties:** -| Context | Properties | Notes | -|---------|-----------|-------| -| `User` | `Id`, `AuthenticatedUserId` | Be mindful of PII | -| `Operation` | `Name`| | -| `Location` | `Ip` | | -| `GlobalProperties` | (dictionary) | Custom key-value pairs | - -**Example: Setting context on a telemetry item** - -```csharp -var request = new RequestTelemetry -{ - Name = "ProcessOrder", - Timestamp = DateTimeOffset.UtcNow, - Duration = TimeSpan.FromMilliseconds(150), - ResponseCode = "200", - Success = true -}; - -// Set user context (be mindful of PII) -request.Context.User.Id = userId; -request.Context.User.AuthenticatedUserId = authenticatedUserId; - -// Set operation context -request.Context.Operation.Name = "ProcessOrder"; - -// Set location context -request.Context.Location.Ip = clientIpAddress; - -// Set custom properties via GlobalProperties -request.Context.GlobalProperties["Environment"] = "Production"; -request.Context.GlobalProperties["DataCenter"] = "WestUS"; - -telemetryClient.TrackRequest(request); -``` - -**Recommended: Use OpenTelemetry Resource Attributes** - -For service-level context (cloud role, version, environment), the recommended approach in 3.x is to use OpenTelemetry Resource attributes, which apply to all telemetry automatically: - -```csharp -configuration.ConfigureOpenTelemetryBuilder(builder => -{ - builder.ConfigureResource(r => r - .AddService( - serviceName: "OrderProcessingService", // Maps to Cloud.RoleName - serviceVersion: "1.2.3", // Maps to Application Version - serviceInstanceId: Environment.MachineName) // Maps to Cloud.RoleInstance - .AddAttributes(new Dictionary - { - ["deployment.environment"] = "Production" - })); -}); -``` - ### Dependency Injection In applications using Microsoft.Extensions.DependencyInjection: diff --git a/examples/BasicConsoleApp/Program.cs b/examples/BasicConsoleApp/Program.cs index f2cd22afcf..840484a428 100644 --- a/examples/BasicConsoleApp/Program.cs +++ b/examples/BasicConsoleApp/Program.cs @@ -19,7 +19,7 @@ static void Main(string[] args) { var telemetryConfig = TelemetryConfiguration.CreateDefault(); - telemetryConfig.ConnectionString = "InstrumentationKey=cfd11a0c-b911-4de5-885d-659e2317e020;IngestionEndpoint=https://westus2-2.in.applicationinsights.azure.com/;LiveEndpoint=https://westus2.livediagnostics.monitor.azure.com/;ApplicationId=50c16cf6-ec05-41ce-a7e7-c377548d53ef"; + telemetryConfig.ConnectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000"; telemetryConfig.SamplingRatio = 1.0f; // Set to 100% for testing; adjust as needed for production telemetryConfig.ConfigureOpenTelemetryBuilder(builder => builder.WithTracing(tracing => tracing.AddSource("MyCompany.MyProduct.MyLibrary").AddConsoleExporter()) From 7d23d459d6eda990fe735feecb60dc5098652a80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:53:50 +0000 Subject: [PATCH 14/15] Replace Assert.Fail with Assert.True(false, ...) for xUnit v2 compatibility Agent-Logs-Url: https://github.com/microsoft/ApplicationInsights-dotnet/sessions/3f259831-d7e7-4b98-8893-d919119e312b Co-authored-by: harsimar <19897860+harsimar@users.noreply.github.com> --- .../TelemetryContextLogProcessorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryContextLogProcessorTests.cs b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryContextLogProcessorTests.cs index 591c528604..caeb7f2dc2 100644 --- a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryContextLogProcessorTests.cs +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryContextLogProcessorTests.cs @@ -549,7 +549,7 @@ private static void AssertAttributeValue(object expected, LogRecord logRecord, s } } - Assert.Fail($"Attribute '{key}' not found in log record."); + Assert.True(false, $"Attribute '{key}' not found in log record."); } private static void ForceWarmupComplete(TelemetryContextLogProcessor processor) From 97e9ae9b6b0d1656065e53f0e8f1ed9c9df430e7 Mon Sep 17 00:00:00 2001 From: Harsimar Kaur Date: Mon, 30 Mar 2026 14:51:46 -0700 Subject: [PATCH 15/15] pr comments --- .../Stable/PublicAPI.Shipped.txt | 2 ++ .../Stable/PublicAPI.Unshipped.txt | 2 -- .../DataContracts/TelemetryContext.cs | 24 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Shipped.txt b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Shipped.txt index 8e7e01dff8..9853129d93 100644 --- a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Shipped.txt +++ b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Shipped.txt @@ -146,6 +146,8 @@ Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.Authentic Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.AuthenticatedUserId.set -> void Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.Id.get -> string Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.Id.set -> void +Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.UserAgent.get -> string +Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.UserAgent.set -> void Microsoft.ApplicationInsights.Extensibility.IOperationHolder Microsoft.ApplicationInsights.Extensibility.IOperationHolder.Telemetry.get -> T Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration diff --git a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt index bcb95412f4..546aede6f2 100644 --- a/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.ApplicationInsights.dll/Stable/PublicAPI.Unshipped.txt @@ -16,8 +16,6 @@ Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext.Id.get Microsoft.ApplicationInsights.Extensibility.Implementation.SessionContext.Id.set -> void Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.AccountId.get -> string Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.AccountId.set -> void -Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.UserAgent.get -> string -Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext.UserAgent.set -> void Microsoft.ApplicationInsights.DataContracts.TelemetryContext.Component.get -> Microsoft.ApplicationInsights.Extensibility.Implementation.ComponentContext Microsoft.ApplicationInsights.Extensibility.Implementation.ComponentContext Microsoft.ApplicationInsights.Extensibility.Implementation.ComponentContext.Version.get -> string diff --git a/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs b/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs index 0e666a6d80..a538a30bc9 100644 --- a/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs +++ b/BASE/src/Microsoft.ApplicationInsights/DataContracts/TelemetryContext.cs @@ -77,35 +77,35 @@ public LocationContext Location } /// - /// Gets the object describing the device tracked by this . + /// Gets the object describing the cloud tracked by this . /// - public DeviceContext Device + public CloudContext Cloud { - get { return LazyInitializer.EnsureInitialized(ref this.device, () => new DeviceContext(default)); } + get { return LazyInitializer.EnsureInitialized(ref this.cloud, () => new CloudContext()); } } /// - /// Gets the object describing a user session tracked by this . + /// Gets the object describing the component tracked by this . /// - public SessionContext Session + public ComponentContext Component { - get { return LazyInitializer.EnsureInitialized(ref this.session, () => new SessionContext()); } + get { return LazyInitializer.EnsureInitialized(ref this.component, () => new ComponentContext()); } } /// - /// Gets the object describing the component tracked by this . + /// Gets the object describing the device tracked by this . /// - public ComponentContext Component + public DeviceContext Device { - get { return LazyInitializer.EnsureInitialized(ref this.component, () => new ComponentContext()); } + get { return LazyInitializer.EnsureInitialized(ref this.device, () => new DeviceContext(default)); } } /// - /// Gets the object describing the cloud tracked by this . + /// Gets the object describing a user session tracked by this . /// - public CloudContext Cloud + public SessionContext Session { - get { return LazyInitializer.EnsureInitialized(ref this.cloud, () => new CloudContext()); } + get { return LazyInitializer.EnsureInitialized(ref this.session, () => new SessionContext()); } } } } \ No newline at end of file