.NET HTTP integration
Telemetry does not publish a native .NET SDK. Use the HTTP API through an application-owned HttpClient or IHttpClientFactory configuration when an ASP.NET Core service, worker, or scheduled job needs structured event ingestion.
The application remains responsible for connection reuse, timeouts, retries, serialization, cancellation, and whether an observability failure may affect the customer workflow.
Configure a named client
builder.Services.AddHttpClient("telemetry", client =>
{
client.BaseAddress = new Uri("https://api.telemetry.sh");
client.Timeout = TimeSpan.FromSeconds(3);
});
builder.Services.AddHttpClient("telemetry-query", client =>
{
client.BaseAddress = new Uri("https://api.telemetry.sh");
client.Timeout = TimeSpan.FromSeconds(15);
});
Resolve the client from IHttpClientFactory instead of constructing a new HttpClient for every event. Store the Telemetry key in the service's existing secret manager and use separate ingest-only and read-only keys where the runtime supports both ingestion and querying.
Send one structured event
Build a typed, allowlisted payload after the final application outcome is known:
using System.Net.Http.Json;
using System.Text.Json;
var payload = new
{
table = "api_request_completed",
data = new
{
event_id = eventId,
route_template = routeTemplate,
method = requestMethod,
status_code = completedStatusCode,
status = completedStatusCode < 400 ? "success" : "error",
latency_ms = elapsed.TotalMilliseconds,
request_id = requestId,
release
}
};
try
{
using var request = new HttpRequestMessage(HttpMethod.Post, "/log")
{
Content = JsonContent.Create(payload)
};
request.Headers.TryAddWithoutValidation("Authorization", telemetryApiKey);
var client = httpClientFactory.CreateClient("telemetry");
using var response = await client.SendAsync(request, cancellationToken);
if (!response.IsSuccessStatusCode)
{
RecordTelemetryDeliveryFailure("telemetry_delivery_rejected");
}
}
catch (OperationCanceledException)
{
RecordTelemetryDeliveryFailure("telemetry_delivery_cancelled");
}
catch (Exception error) when (error is HttpRequestException
or JsonException or NotSupportedException
or ArgumentException or InvalidOperationException)
{
RecordTelemetryDeliveryFailure("telemetry_delivery_failed");
}
static void RecordTelemetryDeliveryFailure(string reason)
{
// Diagnostics must not replace the already completed application result.
try { Console.Error.WriteLine(reason); }
catch { }
}
This example runs after the application operation has completed. Supply its actual route template, method and HTTP status, not fixed success values. Delivery cancellation, transport and serialization failures are contained; the caller cancellation token stays cancelled. It sends once, does not read or log the response body, and does not guarantee storage. Use an application-owned safe diagnostic sink if needed. Verify delivery with SQL.
Use route templates, stable internal identifiers, and controlled error categories. Do not serialize headers, cookies, credentials, request bodies, bound SQL values, stack traces, prompts, generated content, or unrestricted exception messages.
Run SQL
Use a longer, separately bounded query deadline and a read-scoped key:
Resolve the separate telemetry-query client below. It has a 15-second timeout; the ingestion client stays at three seconds. A shorter caller cancellation deadline still takes precedence. Do not change the shared client timeout after it has started a request.
var queryPayload = new
{
query = """
SELECT route_template, COUNT(*) AS requests
FROM api_request_completed
WHERE timestamp_utc >= now() - INTERVAL '24 hours'
GROUP BY route_template
ORDER BY requests DESC
"""
};
using var queryRequest = new HttpRequestMessage(HttpMethod.Post, "/query")
{
Content = JsonContent.Create(queryPayload)
};
queryRequest.Headers.TryAddWithoutValidation(
"Authorization",
telemetryReadApiKey
);
var queryClient = httpClientFactory.CreateClient("telemetry-query");
using var queryResponse = await queryClient.SendAsync(
queryRequest,
queryCancellationToken
);
Check the HTTP status and the response status, data, and key_order fields. An empty result is a valid result and should not be treated as a transport error.
Failure and retry policy
Retry only bounded transient failures such as connection errors, 429, 502, 503, and 504. Apply exponential backoff with jitter, cap total elapsed time, respect request cancellation, and reuse the same event_id. Do not retry an unchanged schema error or bad credential.
For normal product analytics, do not turn a successful customer operation into an error because telemetry delivery failed. For billing or approved audit events that require durability, commit an outbox record with the business transaction and deliver it from a background worker.
Verify the integration
Send known success, failure, retry, cancellation, and timeout fixtures, then inspect recent rows:
SELECT timestamp_utc, event_id, route_template, method, status_code, status, latency_ms
FROM api_request_completed
ORDER BY timestamp_utc DESC
LIMIT 20;
Confirm one terminal row per logical outcome, stable field types, expected units, UTC timestamps, and the absence of sensitive content. Exercise a Telemetry timeout and graceful worker shutdown before enabling the path at production volume.
Continue with the .NET and Serilog integration, Log API, rate limits and errors, and batching and shutdown.