Logging and tracing
Fluxzy.Core emits structured logs through Microsoft.Extensions.Logging.Abstractions and per-exchange traces through System.Diagnostics.ActivitySource. Both are wired through one optional parameter on the Proxy constructor, so adoption is incremental: existing callers see no behaviour change, and new callers opt in by passing a loggerFactory.
Note
Logging support requires Fluxzy.Core 1.36 or later. Earlier versions silently no-op the new APIs.
Quickstart
The minimal opt-in is a single extra constructor argument:
using System.Net;
using Fluxzy;
using Microsoft.Extensions.Logging;
using var loggerFactory = LoggerFactory.Create(builder => builder
.SetMinimumLevel(LogLevel.Debug)
.AddSimpleConsole(options => options.IncludeScopes = true));
var setting = FluxzySetting.CreateLocalRandomPort();
await using var proxy = new Proxy(setting, loggerFactory: loggerFactory);
proxy.Run();
Console.WriteLine("Press any key to exit...");
Console.ReadLine();
When loggerFactory is null (the default), NullLoggerFactory.Instance is used and no logs are emitted. There is zero overhead and zero behaviour change for existing callers.
IncludeScopes = true is important: a lot of context (connection id, exchange id, target authority, HTTP method) is published on the scope rather than baked into individual messages. Backends like Serilog, Seq or Datadog pick those up automatically.
Logging scopes
Every event is fired inside two nested scopes. Each property is attached to every line emitted while the scope is open, so message templates stay terse.
Connection scope
Opened once per inbound TCP connection.
| Property | Meaning |
|---|---|
ProxyConnectionId |
Per-Proxy-instance monotonic counter |
DownstreamRemote |
Inbound IPEndPoint from the client |
DownstreamLocal |
Inbound IPEndPoint Fluxzy is bound to |
Exchange scope
Opened once per HTTP exchange (one request and response pair).
| Property | Meaning |
|---|---|
ExchangeId |
Exchange.Id (monotonic) |
Authority |
Target host:port |
Method |
HTTP method (GET, POST, ...) |
Path |
Request path |
HttpVersion |
HTTP/1.1, HTTP/2, ... |
Event catalogue
EventIds are organised in stable ranges. Treat them as a contract: a renumbered event is a breaking change.
| Range | Tier |
|---|---|
| 1000 to 1099 | Lifecycle (connection and exchange), Debug |
| 1100 to 1199 | Pool and transport, Debug |
| 1200 to 1299 | TLS (reserved for future use) |
| 1099 | Exchange envelope (full headers), Trace |
| 2000 to 2999 | Warnings |
| 3000 to 3999 | Errors |
The full per-event property list lives in docs/logging.md on the main repository. The events you will hit most often:
| EventId | Name | Level | Notes |
|---|---|---|---|
| 1001 | ClientConnectionAccepted |
Debug | Inbound TCP accepted |
| 1002 | RequestResolutionStarted |
Debug | Method, full URL, content length |
| 1003 | DnsResolved |
Debug | Resolver used, latency, forced IP flag |
| 1004 | ConnectionPoolResolved |
Debug | Pool type, ALPN, TLS protocol, cipher suite, SNI |
| 1005 | RequestSending |
Debug | Header length, chunked, Expect-100 |
| 1006 | RequestSent |
Debug | Send timings, body bytes |
| 1007 | ResponseHeaderReceived |
Debug | Status code, TTFB, content type, server header |
| 1008 | ExchangeCompleted |
Debug | End-to-end latency and a phase breakdown |
| 1009 | ConnectionEvicted |
Debug | Pool removed (for example on H2 GoAway) |
| 1010 | ConnectionOpened |
Debug | New upstream connection ready |
| 1099 | ExchangeEnvelope |
Trace | Full request and response headers |
| 2001 | ClientConnectionInitFailed |
Warning | Inbound TLS or CONNECT failed |
| 3001 | ConnectionProcessingError |
Error | Top-level catch in the orchestrator |
1008 ExchangeCompleted is the single most useful line for postmortem analysis: it carries TotalMs, DnsMs, GetPoolMs, TcpConnectMs, TlsHandshakeMs, SendMs, TtfbMs, ResponseBodyMs, TotalSent, TotalReceived, ErrorCount and Aborted in a single record.
Filtering by EventId
You can wire a filter that keeps only the high-signal events:
using var loggerFactory = LoggerFactory.Create(builder => builder
.SetMinimumLevel(LogLevel.Debug)
.AddFilter("Fluxzy", (id, _) => id is 1008 or 2001 or 3001)
.AddSimpleConsole(options => options.IncludeScopes = true));
Sensitive header redaction
EventId 1099 ExchangeEnvelope (Trace) emits the full request and response headers on a single line. Two FluxzySetting properties govern what is shown:
| Property | Default | Effect |
|---|---|---|
LogIncludeSensitiveHeaders |
false |
When false, redacted headers are replaced with <redacted, len=N>. |
LogRedactedHeaders |
Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-Auth-Token |
Case-insensitive name set used by the redactor. |
var setting = FluxzySetting.CreateLocalRandomPort();
setting.LogRedactedHeaders.Add("X-Internal-Token");
If you genuinely need to inspect raw values during debugging, set LogIncludeSensitiveHeaders = true, but be aware that headers will then appear verbatim in any sink that consumes Trace-level logs.
Tracing with ActivitySource
Fluxzy.Core publishes one Activity per processed exchange under the source name Fluxzy.Core. Register it with OpenTelemetry like any other source:
using OpenTelemetry;
using OpenTelemetry.Trace;
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource("Fluxzy.Core")
.AddOtlpExporter()
.Build();
var setting = FluxzySetting.CreateLocalRandomPort();
await using var proxy = new Proxy(setting);
proxy.Run();
No loggerFactory is required for tracing; the ActivitySource is always live. Operation name is "HTTP {Method}", ActivityKind is Server.
OpenTelemetry semantic tags
Tags follow OpenTelemetry HTTP semantic conventions where they map cleanly:
http.request.method, url.full, server.address, server.port, client.address, client.port, user_agent.original, network.protocol.version, http.response.status_code, http.request.body.size, http.response.body.size.
Fluxzy-specific tags
For information that does not have a standard OTel mapping:
fluxzy.exchange_id, fluxzy.pool.type, fluxzy.pool.reused, fluxzy.dns.duration_ms, fluxzy.dns.forced.
ActivityStatusCode is set to Error for 5xx, aborted or errored exchanges, and Ok for 2xx and 3xx responses.
W3C Trace Context propagation
If the inbound HTTP request carries a traceparent header (and optionally tracestate), Fluxzy reads it and starts the per-exchange Activity as a child of that context. Traces therefore stitch end-to-end across upstream callers and downstream origins without any additional configuration on your side.
Putting it all together
A complete program with both logs and traces, sharing the same exchange identifiers:
using System.Net;
using Fluxzy;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Trace;
using var loggerFactory = LoggerFactory.Create(builder => builder
.SetMinimumLevel(LogLevel.Debug)
.AddSimpleConsole(options => options.IncludeScopes = true));
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource("Fluxzy.Core")
.AddOtlpExporter()
.Build();
var setting = FluxzySetting
.CreateDefault(IPAddress.Loopback, 8000);
await using var proxy = new Proxy(setting, loggerFactory: loggerFactory);
proxy.Run();
Console.WriteLine("Proxy listening on 127.0.0.1:8000. Press any key to exit...");
Console.ReadLine();
The OTLP exporter ships traces to any collector listening on the default localhost:4317 endpoint. Pointing it elsewhere, or swapping in AddConsoleExporter() for local debugging, only changes the registration call above; no Fluxzy code needs to change.
Note
Both subsystems are independent. You can enable logging without tracing, tracing without logging, or both at once. Whatever you do not register has zero cost.