| File: Implementation\BreezeWriter.cs | Web Access |
| Project: src\sdk\src\Cli\Microsoft.DotNet.Cli.Telemetry\Microsoft.DotNet.Cli.Telemetry.csproj (Microsoft.DotNet.Cli.Telemetry) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization.Metadata; namespace Microsoft.DotNet.Cli.Telemetry.Implementation; /// <summary> /// Shared primitives for writing Application Insights "Breeze" telemetry envelopes as /// newline-delimited JSON. Used by both the trace serializer /// (<see cref="AzureMonitorTelemetrySerializer"/>) and the log serializer /// (<see cref="AzureMonitorLogSerializer"/>) so the envelope framing, resource-tag mapping, /// property formatting, and timestamp formatting stay identical across signals. /// </summary> internal static class BreezeWriter { private const byte NewLine = (byte)'\n'; /// <summary> /// Serializes <paramref name="envelope"/> using the supplied source-generated /// <paramref name="typeInfo"/> and terminates it with the NDJSON newline separator. /// </summary> public static void WriteEnvelope<TData>(Stream stream, TelemetryEnvelope<TData> envelope, JsonTypeInfo<TelemetryEnvelope<TData>> typeInfo) where TData : class { JsonSerializer.Serialize(stream, envelope, typeInfo); stream.WriteByte(NewLine); } /// <summary> /// Adds the cloud role / role instance / application version / SDK version context tags that /// every envelope carries, derived from the process telemetry resource. /// </summary> public static void AddResourceTags(Dictionary<string, string> tags, TelemetryResourceContext resource) { if (resource.RoleName is not null) { tags[BreezeSchema.CloudRole] = BreezeSchema.Truncate(resource.RoleName, BreezeSchema.CloudRoleMaxLength)!; } if (resource.RoleInstance is not null) { tags[BreezeSchema.CloudRoleInstance] = BreezeSchema.Truncate(resource.RoleInstance, BreezeSchema.CloudRoleInstanceMaxLength)!; } if (resource.ApplicationVersion is not null) { tags[BreezeSchema.ApplicationVersion] = BreezeSchema.Truncate(resource.ApplicationVersion, BreezeSchema.ApplicationVersionMaxLength)!; } tags[BreezeSchema.InternalSdkVersion] = BreezeSchema.Truncate(resource.SdkVersion, BreezeSchema.SdkVersionMaxLength)!; } /// <summary> /// Formats a telemetry property, mirroring the Azure Monitor exporter: drops keys over the /// length limit, drops null values, keeps only the first occurrence of a duplicate key, and /// renders arrays as comma-separated values. /// </summary> public static bool TryFormatProperty(KeyValuePair<string, object?> tag, HashSet<string> seenKeys, out string key, out string value) { key = tag.Key; value = string.Empty; if (tag.Value is null || key.Length > BreezeSchema.KvpMaxKeyLength || !seenKeys.Add(key)) { return false; } var formatted = tag.Value is Array arrayValue ? string.Join(",", arrayValue.Cast<object?>().Select(v => Convert.ToString(v, CultureInfo.InvariantCulture))) : Convert.ToString(tag.Value, CultureInfo.InvariantCulture); value = BreezeSchema.Truncate(formatted, BreezeSchema.KvpMaxValueLength) ?? "null"; return true; } // Formats timestamps in ISO-8601 round-trip ("O") form on a UTC DateTime, which renders the // trailing "Z" designator (e.g. 2026-07-13T18:02:26.2656372Z) exactly as the Azure Monitor // exporter does. Formatting a DateTimeOffset instead would emit "+00:00", which is a valid but // byte-different representation. public static string FormatTime(DateTime utcTimestamp) => DateTime.SpecifyKind(utcTimestamp, DateTimeKind.Utc).ToString("O", CultureInfo.InvariantCulture); public static string FormatTime(DateTimeOffset timestamp) => timestamp.UtcDateTime.ToString("O", CultureInfo.InvariantCulture); }