SimpleL7Proxy captures telemetry for every proxied request, including token usage extracted from streaming AI responses.
Data is emitted to the following configured sinks:
- Azure Application Insights: (Recommended for Production) Set
APPINSIGHTS_CONNECTIONSTRING. Handles structured telemetry (requests, dependencies, exceptions) directly viaTelemetryClient. - Azure Event Hubs: High-volume streaming ingestion. Include
eventhubinEVENT_LOGGERSand setEVENTHUB_CONNECTIONSTRING(orEVENTHUB_NAMESPACEfor managed identity). - Local Log File: JSON event log for debugging/testing. Include
fileinEVENT_LOGGERSand optionally setLOGFILE_NAME. - Console/Stdout: For container logging and local debugging.
Every proxied request produces a ProxyEvent — a key/value dictionary containing request metadata, timing, token usage, and other dimensions. These events are serialized to JSON and dispatched to one or more event logger backends via the CompositeEventClient fan-out.
ProxyEvent.SendEvent()
└── CompositeEventClient.SendData(json)
├── LogFileEventClient.SendData(json) ← if "file" enabled
├── EventHubClient.SendData(json) ← if "eventhub" enabled
└── CustomLogger.SendData(json) ← if custom type enabled
ProxyEventcollects per-request data (status, duration, tokens, headers, etc.).- Common fields (version, revision, container name) are injected from Event Headers (
ICommonEventData). - The event is serialized and passed to
CompositeEventClient.SendData(). CompositeEventClientiterates aFrozenDictionarysnapshot of registered backends — zero-lock, zero-allocation on the hot path.- Each backend buffers events in a
ConcurrentQueueand flushes them asynchronously on a background loop.
| Environment Variable | Default | Description |
|---|---|---|
EVENT_LOGGERS |
file |
Comma-separated list of backends to enable: file, eventhub, or a fully-qualified type name. |
LOGFILE_NAME |
eventslog.json |
Output path for the local file logger. |
LOGTOFILE |
false |
(Legacy) When EVENT_LOGGERS is not set, true → file, false → eventhub. |
EVENTHUB_CONNECTIONSTRING |
— | Connection string for Azure Event Hubs. |
EVENTHUB_NAMESPACE |
— | Event Hub namespace for managed-identity auth (alternative to connection string). |
EVENTHUB_NAME |
— | The specific Event Hub name to write to. |
EVENT_HEADERS |
SimpleL7Proxy.Events.CommonEventHeaders |
Fully-qualified type name of the ICommonEventData implementation that supplies default event fields. |
Use File for local inspection and Event Hub for streaming JSON events to downstream consumers. Both can be enabled together; neither requires replacing the other or disabling Application Insights.
Local Log File (file)
- Writes JSON lines to disk via
LogFileEventClient. - Buffers events and flushes them in batches.
- Suitable for local debugging and testing.
LogFileNamesets the output path inside the proxy process or container; it does not select the file sink. SelectfileinEventLoggersand use a persistent mount when logs must survive container replacement.
EventLoggers=file,eventhub
LogFileName=/logs/proxy-events.json
EventHubName=proxy-events
Note
LogToFile is a legacy fallback, not an additional destination switch. It is consulted only when EventLoggers is unset: true selects File and false selects Event Hub. An explicit EventLoggers value takes precedence; none disables event sinks, not the independently configured Application Insights channel.
Azure Event Hubs (eventhub)
- Streams events to Azure Event Hubs via
EventHubClient. - Supports connection-string auth or managed-identity auth (
EVENTHUB_NAMESPACE). - Use it when another service needs to consume a high-volume event stream. The sink transports JSON events, including request metrics; downstream storage, dashboards, or alerts are the consumer's responsibility.
- Batches events using the Event Hubs SDK
EventDataBatchfor throughput. - If the Event Hub connection fails at startup, the backend is silently disabled and other backends continue.
Both backends are sibling sinks managed by CompositeEventClient — they can run simultaneously. Set EVENT_LOGGERS=file,eventhub to enable both. Each backend self-registers on successful startup; if one fails (e.g., EventHub timeout), the others continue unaffected.
Tip
No file output? Check that File is selected and its path is writable. No Event Hub output? Check the hub name, authentication configuration, and logger startup status; selecting a destination does not provide its credentials.
Custom Event Loggers
Besides the built-in file and eventhub backends, you can create your own event logger by implementing IEventClient and IHostedService in the SimpleL7Proxy assembly.
- Create a class that implements both
IEventClientandIHostedService. - Accept
CompositeEventClient(and any other DI services) in the constructor. - In
StartAsync, perform setup and call_composite.Add(this)to register with the fan-out. - In
SendData, process or forward the JSON event string. - Reference the class by its fully-qualified type name in
EVENT_LOGGERS.
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace SimpleL7Proxy.Events;
public class ConsoleEventLogger : IEventClient, IHostedService
{
private readonly CompositeEventClient _composite;
private readonly ILogger<ConsoleEventLogger> _logger;
public ConsoleEventLogger(CompositeEventClient composite, ILogger<ConsoleEventLogger> logger)
{
_composite = composite;
_logger = logger;
}
public int Count => 0;
public string ClientType => "Console";
public Task StopTimerAsync() => Task.CompletedTask;
public void SendData(string? value)
{
if (!string.IsNullOrEmpty(value))
_logger.LogInformation("[EVENT] {Value}", value);
}
public Task StartAsync(CancellationToken cancellationToken)
{
_composite.Add(this);
_logger.LogInformation("[SERVICE] ✓ ConsoleEventLogger started");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => StopTimerAsync();
}Usage:
EVENT_LOGGERS=file,SimpleL7Proxy.Events.ConsoleEventLogger
The proxy uses ActivatorUtilities.CreateInstance to construct custom loggers, so all constructor dependencies are resolved from DI automatically. If instantiation fails, the error is logged and the remaining loggers continue.
Security: Only types within the
SimpleL7Proxyassembly are resolved. External assemblies cannot be loaded viaEVENT_LOGGERS.
Custom Event Headers
Every event includes a set of default fields (version, revision, container name) that are injected by an ICommonEventData implementation. You can customize these fields by providing your own class.
The built-in CommonEventHeaders class produces:
| Field | Source |
|---|---|
Ver |
Constants.VERSION (build-time) |
Revision |
BackendOptions.Revision |
ContainerApp |
BackendOptions.ContainerApp |
- Create a class that implements
ICommonEventData. - Accept
IOptions<BackendOptions>in the constructor. - Return a
FrozenDictionary<string, string>fromDefaultEventData(). - Set
EVENT_HEADERSto the fully-qualified type name.
using System.Collections.Frozen;
using Microsoft.Extensions.Options;
using SimpleL7Proxy.Config;
namespace SimpleL7Proxy.Events;
public class MyCustomEventHeaders : ICommonEventData
{
private readonly FrozenDictionary<string, string> _data;
public MyCustomEventHeaders(IOptions<BackendOptions> options)
{
var bo = options.Value;
_data = new Dictionary<string, string>
{
["Ver"] = Constants.VERSION,
["Revision"] = bo.Revision,
["ContainerApp"] = bo.ContainerApp,
["Region"] = Environment.GetEnvironmentVariable("AZURE_REGION") ?? "unknown",
["Tenant"] = Environment.GetEnvironmentVariable("TENANT_ID") ?? "default"
}.ToFrozenDictionary();
}
public FrozenDictionary<string, string> DefaultEventData() => _data;
}Usage:
EVENT_HEADERS=SimpleL7Proxy.Events.MyCustomEventHeaders
If the configured EVENT_HEADERS type cannot be found, does not implement ICommonEventData, or throws during construction, the proxy logs a warning and falls back to CommonEventHeaders automatically. The proxy will always start successfully regardless of event header misconfiguration.
Standard gateways cannot count tokens in streaming responses (Server-Sent Events/SSE) because the "usage" field is often only sent in the final chunk, or requires aggregating chunks.
SimpleL7Proxy uses a specialized Stream Processor to parse response bodies on-the-fly without buffering the full response (which would add latency).
For requests routed to a host with processor=OpenAI configured, the following metrics are automatically extracted and logged:
| Metric Field | Description |
|---|---|
Usage.Prompt_Tokens |
Number of tokens in the input prompt. |
Usage.Completion_Tokens |
Number of tokens generated in the response. |
Usage.Total_Tokens |
Total billable tokens for the request. |
These metrics appear in the Custom Dimensions of the Request or Event telemetry in Application Insights.
Kusto Query Example (App Insights):
requests
| where customDimensions contains "Usage.Total_Tokens"
| project
timestamp,
Duration = duration,
TotalTokens = toint(customDimensions["Usage.Total_Tokens"]),
PromptTokens = toint(customDimensions["Usage.Prompt_Tokens"]),
Model = customDimensions["Model"]
| summarize avg(TotalTokens) by bin(timestamp, 1h)Every request includes standard fields useful for operational monitoring:
S7P_RequestId: Unique correlation ID.BackendHost: The specific backend URL that handled the request.S7P_Priority: The priority queue assigned to the request.CircuitBreakerStatus: Whether the host was healthy.Retries: Number of Retry attempts performed.
LogAllRequestHeaders/LogAllResponseHeaders: Enable full header capture for debugging (be careful with PII/Secrets).LogAllRequestHeadersExcept: Blacklist sensitive headers (e.g.,Authorization,api-key) to prevent leaking credentials.