agentleFS
LeanMeanCleanAppsLeanMeanCleanApps/CleanModelContextProtocol/.agents/skills/serilog/SKILL.md

serilog

Skill1 starsChanged 7 days agoMITView on GitHub

How logging works with Serilog - registration with AddSerilog, configuration from appsettings, sinks per environment, logs over OTLP, the enrichers and properties on every event, the request log, the LoggingBehaviour, who logs an unexpected exception, levels, and source-generated log messages. Use when adding a log line, changing log configuration or levels, adding a property, or looking for where logs go.

2 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

6 rules. About 0 are common, 1 is distinctive, and 5 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Every event carries these properties:

    line 23
  2. Very specific:

    Write each message as a [LoggerMessage] method on a partial class, with named holes in the template.

    line 49
  3. Very specific:

    Add a sink or change a level in the appsettings files.

    line 52
  4. Very specific:

    Use string interpolation in a log message ($"Created {id}").

    line 97
  5. Very specific:

    Call logger.LogInformation(...) and its friends directly.

    line 98
  6. Very specific:

    Add a second request logger, such as UseHttpLogging.

    line 104

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Serilog

The rule

Serilog is the logger. Application and Infrastructure code log through Microsoft.Extensions.Logging.ILogger<T>, and Serilog does the writing. The presentation layer's startup registers it with builder.Services.AddSerilog(...), which reads the Serilog section of the appsettings files and also sets the static Log.Logger that Program.cs uses for the start and fatal messages.

EnvironmentSinkWhere to look
localConsole, with the output template in appsettings.local.jsonThe terminal
dockerConsole, and OTLP through the collector to the Aspire dashboardThe Aspire dashboard, on the port docs/RUN.md lists, or docker compose logs
Lambda, anywhereConsole, as compact JSON, and OTLP when the endpoint is setCloudWatch Logs, or docker compose logs for the emulator, and the Aspire dashboard in Docker Compose
Any, when OTEL_EXPORTER_OTLP_ENDPOINT is setAlso OTLP, through Serilog.Sinks.OpenTelemetryWhatever receives the OTLP (telemetry)

The OTLP sink is added in code, not in the appsettings files, because it follows the standard OTEL_* settings like the trace and metric exporters do. It sends the same service.name, service.version and deployment.environment.name as the traces, and each event carries the trace and span IDs of whatever was running when it was written, so a log line links to its trace.

Every event carries these properties:

PropertyComes from
ApplicationNameSerilog:Properties in the environment's appsettings
EnvironmentName, MachineNameSerilog.Enrichers.Environment, listed under Serilog:Enrich
AssemblyVersion, AssemblyInformationalVersionAdded in code at startup
Anything pushed with LogContextFromLogContext

Levels are set in configuration, never in code: Information by default and Debug in local, with Information for Microsoft, Warning for Microsoft.AspNetCore and Microsoft.EntityFrameworkCore.Database.Command (a line per SQL statement buries everything else, and EF Core's meter already counts them), and Error for System. The built-in Logging section is set to None so nothing is written twice.

Three things log a request, and each says something the others don't:

WhatWhereLogs
The request logUseSerilogRequestLogging(), in web templates onlyOne line per request with its method, path, status code and time. It's registered before the exception handler, so it logs the status code the client actually got
LoggingBehavioursrc/Application/Common/Behaviours/, around every use caseInformation when a request is handled, fails validation or finds nothing, with the request name and its time in milliseconds
The presentation error handlerThe exception handler or error filterError, with the exception, for anything it reports as unexpected

An unexpected exception is logged once, by the class that turns it into a response, because handling it stops the framework logging it. LoggingBehaviour leaves it alone. In Lambda the exception goes back to the caller as a function error and the runtime writes it to CloudWatch Logs, so nothing logs it a second time.

The Lambda template has no appsettings files. Its startup sets the same levels and properties in code, and environment variables such as Serilog__MinimumLevel__Default override them.

Steps

  1. Take ILogger<T> through the constructor where you need it. Never reference Serilog types outside the presentation layer.
  2. Write each message as a [LoggerMessage] method on a partial class, with named holes in the template. The source generator writes the code, so nothing is formatted when the level is off. Put the methods in a #region Log Messages at the bottom of the class.
  3. Pick the level for the reader at 3am: Information for something that happened, Warning for something odd that was handled, Error for a failed request, Debug for detail you'd only want while developing.
  4. Name new properties in PascalCase with no spaces, so they can be used in templates and queried in any backend without quoting.
  5. Add a sink or change a level in the appsettings files. A new sink package goes in Directory.Packages.props (packages-and-dependencies).
  6. In the Aspire dashboard's structured logs, filter on any property, or open a log's trace from its trace ID.

Example

// src/Application/Common/Behaviours/LoggingBehaviour.cs
[LoggerMessage(Level = LogLevel.Information, Message = "{RequestName} handled in {ElapsedMilliseconds:0.0} ms")]
private static partial void LogHandled(ILogger logger, string requestName, double elapsedMilliseconds);
// src/Presentation/Extensions, where startup registers services
_ = builder.Services.AddSerilog((services, loggerConfiguration) =>
{
    var configuration = services.GetRequiredService<IConfiguration>();

    _ = loggerConfiguration.ReadFrom.Configuration(configuration)
        .Enrich.WithProperty(
            "AssemblyVersion",
            assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version);

    if (telemetry.Enabled)
    {
        _ = loggerConfiguration.WriteTo.OpenTelemetry(
            options =>
            {
                options.ResourceAttributes = telemetry.ResourceAttributes;
                options.OnBeginSuppressInstrumentation = SuppressInstrumentationScope.Begin;
            },
            key => configuration[key]);
    }
});
// src/Presentation/appsettings.docker.json
"Serilog": {
  "WriteTo": [ { "Name": "Console", "Args": { "outputTemplate": "..." } } ],
  "Properties": { "ApplicationName": "CleanModelContextProtocol-Docker" }
}

Don't

  • Use string interpolation in a log message ($"Created {id}"). It loses the property and allocates on every call.
  • Call logger.LogInformation(...) and its friends directly. Write a [LoggerMessage] method instead.
  • Log an exception that something else will log, such as one you rethrow.
  • Log tokens, connection strings, passwords or personal data.
  • Use Console.WriteLine.
  • Reference Serilog from Application or Infrastructure.
  • Register Serilog with builder.Host.UseSerilog. AddSerilog is the current registration.
  • Add a second request logger, such as UseHttpLogging. Requests are logged once, by Serilog.
  • Move the request log after the exception handler. It would log every handled error as a 500.

Check

dotnet run --project ./src/Presentation   # watch the console, then make a request
docker compose up -d --build              # then open the Aspire dashboard on the port docs/RUN.md lists
dotnet test tests/Application.Tests.Unit  # LoggingBehaviourTests check the messages with FakeLogger

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.