| File: Implementation\HttpTelemetryUploadTransport.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.IO.Compression; using System.Net; using System.Net.Http.Headers; namespace Microsoft.DotNet.Cli.Telemetry.Implementation; /// <summary> /// Uploads telemetry payloads to the Azure Monitor Breeze <c>/v2.1/track</c> endpoint with a /// plain <see cref="HttpClient"/>. The request body is the newline-delimited JSON produced by /// <see cref="AzureMonitorTelemetrySerializer"/>, gzip-compressed on the wire (matching the /// Azure Monitor exporter). Persisted blobs stay uncompressed so a partially-accepted payload /// can be re-sliced by envelope index. /// </summary> internal sealed class HttpTelemetryUploadTransport : ITelemetryUploadTransport { // A single shared client for the process. Timeout is set to InfiniteTimeSpan because // cancellation is controlled by the CancellationToken passed to each upload call — // either from the provider's Shutdown budget or from the drain loop's per-blob budget. private static readonly HttpClient s_httpClient = new() { Timeout = System.Threading.Timeout.InfiniteTimeSpan }; private readonly Uri _trackUri; private readonly HttpClient _client; public HttpTelemetryUploadTransport(Uri trackUri) : this(trackUri, s_httpClient) { } // Test hook: uploads through a caller-supplied handler (its own HttpClient) so gzip and // 206 handling can be exercised without a real network call. internal HttpTelemetryUploadTransport(Uri trackUri, HttpMessageHandler handler) : this(trackUri, new HttpClient(handler) { Timeout = System.Threading.Timeout.InfiniteTimeSpan }) { } private HttpTelemetryUploadTransport(Uri trackUri, HttpClient client) { _trackUri = trackUri; _client = client; } public async Task<TelemetryUploadResult> TryUploadAsync(byte[] payload, CancellationToken cancellationToken) { using var content = new GzipJsonContent(payload); using var request = new HttpRequestMessage(HttpMethod.Post, _trackUri) { Content = content }; request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); using var response = await _client.SendAsync(request, cancellationToken).ConfigureAwait(false); if (response.StatusCode == HttpStatusCode.OK) { return TelemetryUploadResult.Accepted; } if (response.StatusCode == HttpStatusCode.PartialContent) { var body = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); var trackResponse = BreezePartialContent.ParseResponse(body); var retriable = BreezePartialContent.GetRetriablePayload(payload, trackResponse); // No retriable remainder means every rejected item was permanently rejected, so the // blob is done. Otherwise re-persist just the retriable envelopes. return retriable is null ? TelemetryUploadResult.Accepted : TelemetryUploadResult.PartiallyAccepted(retriable); } // Throttling, server errors, etc.: retain the blob and retry it later. return TelemetryUploadResult.Rejected; } /// <summary> /// An <see cref="HttpContent"/> that gzip-compresses its source payload directly into the /// request stream, so the compressed bytes are never materialized in a separate buffer. The /// compressed length is not known up front, so the request uses chunked transfer-encoding. /// </summary> private sealed class GzipJsonContent : HttpContent { private readonly ReadOnlyMemory<byte> _payload; public GzipJsonContent(ReadOnlyMemory<byte> payload) { _payload = payload; Headers.ContentType = new MediaTypeHeaderValue("application/json"); Headers.ContentEncoding.Add("gzip"); } protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context) { using var gzip = new GZipStream(stream, CompressionLevel.Optimal, leaveOpen: true); await gzip.WriteAsync(_payload).ConfigureAwait(false); } // The compressed size isn't known without compressing first; stream with chunked encoding. protected override bool TryComputeLength(out long length) { length = 0; return false; } } }