| File: PersistentStorageLogExporter.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; using OpenTelemetry.Logs; namespace Microsoft.DotNet.Cli.Telemetry; /// <summary> /// The log counterpart of <see cref="PersistentStorageTraceExporter"/>: an OpenTelemetry log /// exporter that maps each batch of <see cref="LogRecord"/> instances to the Application /// Insights wire format and persists it to durable on-disk storage instead of transmitting it, /// so a short-lived CLI process captures its log telemetry before exiting. /// /// Like the trace exporter, it owns the persist-then-drain pipeline: the first time it exports /// it starts a background <see cref="PersistentStorageTelemetryUploader"/> that opportunistically /// uploads telemetry persisted by this and previous CLI invocations. Because it drains the same /// storage the trace exporter uses, either exporter's drain uploads every persisted blob (leasing /// prevents double-upload), so running both signals against one storage directory is safe. /// /// This exporter should be driven by a <c>SimpleLogRecordExportProcessor</c> so that /// <see cref="Export"/> runs synchronously as each log record is emitted, guaranteeing the write /// completes before process shutdown. /// </summary> internal sealed class PersistentStorageLogExporter : BaseExporter<LogRecord> { 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 PersistentStorageLogExporter( 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<LogRecord> batch) { try { StartBackgroundDrainOnce(); var resource = _resourceContext ??= TelemetryResourceContextFactory.FromResource(ParentProvider?.GetResource()); var bytes = AzureMonitorLogSerializer.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) { _drainCts?.Cancel(); if (_drainTask is not null) { try { return _drainTask.Wait(timeoutMilliseconds); } catch (AggregateException) { 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()); } }); } }