Summary#
Log.* writes a line to your app's structured log — the one osy logs reads back.
Log.Information("Order {OrderCode} shipped with {Qty} items", order.Code, order.Qty);A log line is not just text. The {OrderCode} and {Qty} slots become searchable fields on the stored line, so
you can later ask for every line where OrderCode was A-1, rather than grepping for a substring.
You do not declare a dependency to use it. Logging isn't network access or data access, so — unlike the HTTP and
storage surfaces — there is nothing to switch on. Log.* is simply always there.
Log.* works the same in server-side code and in UI actions — same spelling, same levels, same fields. A line
written in the browser goes to the browser console and to your app's log, so osy logs shows both sides of a click
together.
Signature#
void Log.Verbose ([Exception e, ] string message [, values…])
void Log.Debug ([Exception e, ] string message [, values…])
void Log.Information([Exception e, ] string message [, values…])
void Log.Warning ([Exception e, ] string message [, values…])
void Log.Error ([Exception e, ] string message [, values…])
void Log.Fatal ([Exception e, ] string message [, values…])message— a template with{Field}slots, an interpolated string, or a plain string.values— one value per{Field}in the template, in order. Not used with an interpolated string (its holes already are the values).e(optional, on any level) — an exception to attach to the line, with its stack trace.
The six levels, quietest to loudest: Verbose (tracing detail, normally switched off) · Debug (detail useful while developing) · Information (the app's normal running commentary) · Warning (something is off, but the operation continued) · Error (an operation failed) · Fatal (the app cannot continue).
Description#
Two ways to write a line — both keep the fields#
The template form names its fields explicitly:
Log.Information("Order {OrderCode} shipped with {Qty} items", order.Code, order.Qty);The interpolated form reads more naturally, and captures exactly the same fields:
Log.Information($"Order {order.Code} shipped with {order.Qty} items");Both render the identical message, and both store OrderCode/Qty (template form) or OrderCode/OrderQty
(interpolated form) as fields you can search on. The structure is a bonus you never pay for — the line reads the
same either way.
This is worth dwelling on, because it is the opposite of what most languages do. Elsewhere, putting an interpolated string into a logger destroys the structure: the values are pasted into one flat string before the logger ever sees them, which is why linters warn you away from it. Here the natural spelling is also the correct one.
Field names in the interpolated form are derived from what's in the hole: {order.Code} becomes OrderCode,
{count} becomes Count. A hole that isn't a simple path — {order.Total * rate} — has no natural name, so it is
stored positionally (arg0).
Which form should you use? If a dashboard or an alert depends on a field name, use the template form — the name is written down, so renaming a variable can't silently change it. For everyday logging, the interpolated form is shorter and reads better. Both are first-class.
Formatting a value#
A format specifier works in either form, exactly as it does elsewhere in the language:
Log.Information("Charged {Amount:N2} to {Card}", amount, card);
Log.Information($"Charged {amount:N2} to {card}");A message you computed#
If the message is a value rather than a template, it still logs — as a plain line, with no fields:
var msg = BuildSummary(order);
Log.Information(msg);That is allowed and sometimes exactly right. But there is nothing for values to bind to, so passing extra values alongside a computed message is an error. Switch to a template when you want fields back.
Recording a failure#
Attach the exception itself rather than pasting its message into the text — the stack trace is stored with the line:
try {
ChargeCard(order);
} catch (Exception e) {
Log.Error(e, "Charge failed for {OrderCode}", order.Code);
}What you never have to pass#
Every line automatically carries the context it was written in: which request it belongs to (its correlation id), which app, which user, and which function was running. You never pass any of that — it is added for you, and it is what lets you pull up every line from one request, across everything it touched:
osy logs --corr a1b2c3d4e5f6That holds even when a function pauses and resumes later: both halves belong to the same request.
Lines written from background work — a scheduled task, a workflow step — carry the correlation id of the request that started the work, so a chain that leaves the request and comes back is still one trace.
One click, one trace#
This is the part worth understanding, because it is what the whole surface is for.
When a user clicks something, the action that runs, the log lines it writes, and every server function it calls all share one correlation id. So a bug that starts in the browser and ends in a server function is one query:
osy logs --corr c-a1b2c3d4e5f6You will see the browser's lines and the server's lines interleaved, in order — not two disconnected halves.
That includes the line you most want: an action that throws is logged automatically, at Error, with its exception
and under that same id. You do not have to wrap your actions in try/catch to find out that one failed.
Reading the lines back#
osy logs reads your app's log back from the local platform:
osy logs # the recent lines
osy logs --tail # follow live, printing new lines as they are written
osy logs --level error,warning # only these levels (exact — comma-separated means "any of")
osy logs --function-name ShipOrder # only lines written while this function ran
osy logs --corr a1b2c3d4e5f6 # every line from one request, across everything it touched
osy logs --grep "timed out" # search the message text
osy logs --side client # only the lines your app wrote in the BROWSER
osy logs --side server # only the lines it wrote server-sideThe same command exists against a deployed app as osyrin app logs, with the same filters — except --tail, which is
local-only for now.
A note on browser lines in production#
Lines written in the browser are sent to your app's log, so you can see what a real user's browser actually did. To keep
that from becoming noise (and cost), only Warning and above are sent from a deployed app by default — locally you get
everything. Set Log:ClientMinimumLevel to change it.
Examples#
Log a milestone with searchable fields, and record a failure with its exception:
entity Order {
[Required] string Code;
int Qty;
bool Shipped;
}
void ShipOrder(Order order) {
Log.Information("Order {OrderCode} shipped with {Qty} items", order.Code, order.Qty);
order.Shipped = true;
}
void ChargeOrder(Order order) {
try {
throw new Exception("card declined");
} catch (Exception e) {
// The exception rides along with the line — stack trace and all.
Log.Error(e, "Charge failed for {OrderCode}", order.Code);
}
}The interpolated form — same fields, less ceremony:
void Audit(Order order) {
var qty = order.Qty;
Log.Debug($"Recomputing totals for {order.Code} ({qty} items)");
Log.Warning($"Order {order.Code} has no line items");
}See also#
- Http.* — the outbound-HTTP surface, whose calls appear in these same logs