| File: PersistentStorageTraceExporter.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.Diagnostics; using System.Threading; using Microsoft.DotNet.Cli.Telemetry.Implementation; using OpenTelemetry; namespace Microsoft.DotNet.Cli.Telemetry; /// <summary> /// An OpenTelemetry trace exporter that does not transmit anything itself. Instead it maps /// each batch of spans to the Application Insights wire format and persists it to durable /// on-disk storage, so a short-lived CLI process is guaranteed to capture its telemetry /// before exiting. /// /// The exporter owns the full persist-then-drain pipeline: as soon as it starts exporting /// (i.e. the first span ends) it kicks off a background <see cref="PersistentStorageTelemetryUploader"/> /// that opportunistically uploads telemetry persisted by this and previous CLI invocations. /// Tying the drain to the exporter's own lifecycle means it only runs when telemetry is /// actually enabled, and its behavior is configured through the same exporter options. /// /// This exporter should be driven by a <c>SimpleActivityExportProcessor</c> so that /// <see cref="Export"/> runs synchronously as each span ends, guaranteeing the write /// completes before process shutdown. /// </summary> internal sealed class PersistentStorageTraceExporter : BaseExporter<Activity> { private readonly ITelemetryBlobStorage _storage; private readonly string _instrumentationKey; private readonly Uri _ingestionTrackUri; private readonly int _leasePeriodMilliseconds; private readonly int _maxBlobsPerDrain; private TelemetryResourceContext? _resourceContext; // Guards against starting more than one background drain per exporter. private int _drainStarted; private CancellationTokenSource? _drainCts; private Task? _drainTask; public PersistentStorageTraceExporter( ITelemetryBlobStorage storage, string instrumentationKey, Uri ingestionTrackUri, int leasePeriodMilliseconds, int maxBlobsPerDrain) { _storage = storage; _instrumentationKey = instrumentationKey; _ingestionTrackUri = ingestionTrackUri; _leasePeriodMilliseconds = leasePeriodMilliseconds; _maxBlobsPerDrain = maxBlobsPerDrain; } public override ExportResult Export(in Batch<Activity> batch) { try { // Phase 2: the first time the exporter runs, start the background upload of telemetry // persisted by this and previous invocations. Doing this here (rather than at // construction) ties the drain to the exporter actually being started and keeps it // from running when telemetry is opted out (no spans are exported in that case). StartBackgroundDrainOnce(); var resource = _resourceContext ??= TelemetryResourceContextFactory.FromResource(ParentProvider?.GetResource()); var bytes = AzureMonitorTelemetrySerializer.SerializeBatch(in batch, resource, _instrumentationKey); if (bytes is null || bytes.Length == 0) { return ExportResult.Success; } return _storage.TryPersist(bytes) ? ExportResult.Success : ExportResult.Failure; } catch (Exception e) { // Telemetry must never surface errors to the CLI. Swallow and report failure. Debug.Fail(e.ToString()); return ExportResult.Failure; } } protected override bool OnShutdown(int timeoutMilliseconds) { // Signal the background drain to stop and wait for it to finish within the // remaining shutdown budget. This ensures inflight HTTP POSTs are cancelled and // the drain loop persists any retriable remainders before the process exits. _drainCts?.Cancel(); if (_drainTask is not null) { try { return _drainTask.Wait(timeoutMilliseconds); } catch (AggregateException) { // The drain swallows its own exceptions; this handles edge cases like // ObjectDisposedException from the CTS during shutdown. return true; } } return true; } private void StartBackgroundDrainOnce() { if (Interlocked.Exchange(ref _drainStarted, 1) != 0) { return; } _drainCts = new CancellationTokenSource(); var transport = new HttpTelemetryUploadTransport(_ingestionTrackUri); var uploader = new PersistentStorageTelemetryUploader(_storage, transport, _leasePeriodMilliseconds, _maxBlobsPerDrain); _drainTask = Task.Run(async () => { try { await uploader.DrainAsync(_drainCts.Token).ConfigureAwait(false); } catch (OperationCanceledException) { // Expected when shutdown is signalled. } catch (Exception e) { // Background telemetry drain must never surface errors. Debug.Fail(e.ToString()); } }); } }