Skip to main content

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:

TargetWriter
FileFile.AppendText(path)
StdoutConsole.Out
In-memory testnew StringWriter()
Anything streamablewrap 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 ConsoleSink in dev, JsonSink to 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.