Table of Contents

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.