Log sinks
LogicGridLogger doesn't write anywhere on its own — it formats log
entries and hands them to one or more ILogSink instances. LogicGrid
serves two sinks; you can implement your own to forward
to Serilog, NLog, Datadog, ELK, or anything else.
ILogSink contract
public interface ILogSink
{
void Write(LogicGridLogEntry entry);
}
public sealed class LogicGridLogEntry
{
public DateTimeOffset Timestamp { get; }
public string Level { get; } // "info" | "debug" | "warn" | "error"
public string Category { get; } // "agent" | "admin" | "llm" | "tool" | "retry" | "budget"
public string RunId { get; }
public string Message { get; }
public IDictionary<string, object?> Properties { get; }
}
Write must never throw — handle your own errors. The framework
swallows exceptions just in case, but a bad sink is your bug.
ConsoleSink (default)
Color-coded, human-readable, one line per entry. Used automatically
when you call .WithLogging() without sinks.
09:14:02.118 [INF] [a3f2c891] [Helper] started | input: Capital of France?
09:14:03.402 [INF] [a3f2c891] [Helper] completed | output: Paris. | 1284ms | 18 tokens
var ctx = new AgentContext().WithLogging(); // ConsoleSink under the hood
// Equivalent explicit form:
var ctx = new AgentContext()
.WithLogging(options: null, new ConsoleSink());
JsonSink — newline-delimited JSON
public sealed class JsonSink : ILogSink
{
public JsonSink(TextWriter writer);
}
Each entry becomes a single JSON object on its own line — the format Seq, Loki, Splunk, Promtail, and most log shippers expect.
using LogicGrid.Core.Logging;
using var file = File.AppendText("logs/run.ndjson");
var ctx = new AgentContext()
.WithLogging(
options: new LogicGridLoggerOptions { ShowLlmMessages = false },
sinks: new ILogSink[] { new ConsoleSink(), new JsonSink(file) });
await admin.RunAsync("…");
Sample line in logs/run.ndjson:
{"timestamp":"2026-04-25T09:14:02.118+00:00","level":"info","category":"agent","runId":"a3f2c891","message":"[Helper] started | input: Capital of France?"}
JsonSink accepts any TextWriter:
| Target | Writer |
|---|---|
| File | File.AppendText(path) |
| Stdout | Console.Out |
| In-memory test | new StringWriter() |
| Anything streamable | wrap your Stream in a StreamWriter |
Multiple sinks at once
Pass them all to .WithLogging:
var ctx = new AgentContext()
.WithLogging(
new LogicGridLoggerOptions(),
new ConsoleSink(),
new JsonSink(File.AppendText("run.ndjson")));
Every entry is written to every sink. Sinks run sequentially in the order they're passed.
Custom sink — bridge to Serilog
using Serilog;
public sealed class SerilogSink : ILogSink
{
private readonly ILogger _serilog;
public SerilogSink(ILogger serilog) { _serilog = serilog; }
public void Write(LogicGridLogEntry entry)
{
var level = entry.Level switch
{
"error" => Serilog.Events.LogEventLevel.Error,
"warn" => Serilog.Events.LogEventLevel.Warning,
"debug" => Serilog.Events.LogEventLevel.Debug,
_ => Serilog.Events.LogEventLevel.Information,
};
_serilog
.ForContext("RunId", entry.RunId)
.ForContext("Category", entry.Category)
.Write(level, "{Message}", entry.Message);
}
}
Wire it:
var ctx = new AgentContext()
.WithLogging(options: null, new SerilogSink(Log.Logger));
Custom sink — Datadog HTTP intake
public sealed class DatadogSink : ILogSink
{
private readonly HttpClient _http;
private readonly string _url;
private readonly string _service;
public DatadogSink(HttpClient http, string apiKey, string service)
{
_http = http;
_service = service;
_url = $"https://http-intake.logs.datadoghq.com/api/v2/logs?dd-api-key={apiKey}";
}
public void Write(LogicGridLogEntry entry)
{
var payload = JsonSerializer.Serialize(new
{
ddsource = "logicgrid",
service = _service,
ddtags = $"runid:{entry.RunId},category:{entry.Category}",
status = entry.Level,
message = entry.Message,
timestamp = entry.Timestamp.ToUnixTimeMilliseconds(),
});
_ = _http.PostAsync(_url,
new StringContent(payload, Encoding.UTF8, "application/json"));
}
}
(Fire-and-forget POST — keeps the run hot. For production, batch and back off.)
Tips
- Production preset. Use
ConsoleSinkin dev,JsonSinkto a file in production, plus your APM bridge sink. Three sinks is fine. - Don't block the run. Sinks run on the agent's thread. If your sink does I/O, wrap it in fire-and-forget or a queue.
- Test with
StringWriter. Capture every line written by a run and assert against it. Cheaper than mocking the bus.