Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/configuration-and-environments/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The Lambda template has no appsettings files or environments. Its startup builds
| Environment | Set by | Used for |
| --- | --- | --- |
| `local` | `launchSettings.json` (`DOTNET_ENVIRONMENT=local`) | `dotnet run` on a developer machine: console logs, sample data on |
| `docker` | `docker-compose.yml` and the Dockerfile | `docker compose up`: logs to Seq, database in `/app/data`, sample data on |
| `docker` | `docker-compose.yml` and the Dockerfile | `docker compose up`: telemetry over OTLP through the collector to the Aspire dashboard, database in `/app/data`, sample data on |
| anything else | the host | Real deployments: base settings only, sample data off, HSTS on |

Settings that exist today:
Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/docker-and-compose/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ One multi-stage `Dockerfile` does everything the pipeline needs:
| `publish` | Publishes the Presentation project |
| `run` | The runtime image: non-root `$APP_UID`, `WORKDIR /app`, data in `/app/data`, a `HEALTHCHECK` on `/health/live` |

`docker-compose.yml` runs the app with `DOTNET_ENVIRONMENT=docker`, the `movie-reviews-data` volume and a pinned Seq image for logs. Images are built by CI but not pushed anywhere; the template ships the Dockerfile so users can.
`docker-compose.yml` runs the app with `DOTNET_ENVIRONMENT=docker`, the `movie-reviews-data` volume a pinned OpenTelemetry Collector that receives the app's OTLP logs, traces and metrics, configured by `otel-collector.yaml`, and a pinned Aspire dashboard the collector forwards them to (`telemetry`). The dashboard UI is at <http://localhost:18888>, and the collector's OTLP ports are published on 4317 and 4318 so a `dotnet run` can send to it too. Images are built by CI but not pushed anywhere; the template ships the Dockerfile so users can.

The Lambda template's `run` stage is the AWS Lambda .NET base image instead. It runs as user `65534` from `/var/task`, keeps its database in `/tmp`, and has no `HEALTHCHECK` or Seq. Compose maps the runtime interface emulator to port 9000, so the function can be invoked with a `POST` to `/2015-03-31/functions/function/invocations`.
The Lambda template's `run` stage is the AWS Lambda .NET base image instead. It runs as user `65534` from `/var/task`, keeps its database in `/tmp`, and has no `HEALTHCHECK`. Its compose file runs the collector and the Aspire dashboard as well. Compose maps the runtime interface emulator to port 9000, so the function can be invoked with a `POST` to `/2015-03-31/functions/function/invocations`.

## Steps

Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/health-checks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Two questions, two endpoints, in every template that runs a web host.
| `/health/live` | Is the process up? | No checks. It only fails when the app can't answer at all. |
| `/health/ready` | Can it do its job? | Every check tagged `ready`, which today is the database. |

Both are anonymous. The Docker image's `HEALTHCHECK` calls `/health/live`. Infrastructure owns the checks for what it talks to (`AddInfrastructureHealthChecks` in `src/Infrastructure/DependencyInjection.cs`); Presentation maps the endpoints. The Lambda template has no health endpoints, because Lambda manages its own lifecycle.
Both are anonymous and answer with plain text, because Docker, load balancers and orchestrators call them without a token. Neither is traced (`telemetry`). The Docker image's `HEALTHCHECK` calls `/health/live`. Infrastructure owns the checks for what it talks to (`AddInfrastructureHealthChecks` in `src/Infrastructure/DependencyInjection.cs`); Presentation maps the endpoints. The Lambda template has no health endpoints, because Lambda manages its own lifecycle.

## Steps

Expand All @@ -38,7 +38,7 @@ public static IHealthChecksBuilder AddInfrastructureHealthChecks(this IHealthChe

- Put a dependency check on `/health/live`.
- Require authentication on either endpoint.
- Return version or configuration details from a health endpoint.
- Return version or configuration details from a health endpoint. Anyone can call it; the version is on every span, metric and log as `service.version`, and behind auth at the version endpoint.

## Check

Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/mcp-inspector/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ metadata:
| --- | --- |
| MCP server | <http://localhost:8080/mcp> |
| MCP Inspector | <http://localhost:6274> |
| Seq (logs) | <http://localhost:5341> |
| Aspire dashboard (logs, traces, metrics) | <http://localhost:18888> |

## Steps

1. Start everything: `docker compose up -d --build`.
2. Open <http://localhost:6274>, flip the switch on the server card to connect, then open **Tools**, **Resources** or **Prompts**.
3. After changing a tool, check its description and every argument's description read well, the annotations are right, and a call returns the result model you expect.
4. Try the failure paths too: an unknown id, a star rating of 6. The model should get a message it can act on.
5. Find the request in Seq if something looks wrong (`serilog`).
5. Find the tool call in the Aspire dashboard if something looks wrong: its trace shows the tool, the use case and each SQL command, and its logs sit beside it (`telemetry`).
6. With authentication on, the Inspector needs a token from your authority. Leave `Authentication:Authority` empty for local checks.

## Don't
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/mediator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Requests travel from the presentation layer to Application through Mediator (`Me

Every request uses `IRequest<TResponse>`. Mediator also offers `ICommand` and `IQuery` markers; we don't use them, because nothing yet needs a behaviour that applies to only one kind.

Behaviours run in the order they're registered. The first one registered is the outermost, so it runs first on the way in and last on the way out. `LoggingBehaviour` is registered first, so it times and logs everything inside it, validation failures included (`serilog`). `ValidationBehaviour` comes next, so invalid requests never reach a handler.
Behaviours run in the order they're registered. The first one registered is the outermost, so it runs first on the way in and last on the way out. `LoggingBehaviour` is registered first, so it times and logs everything inside it, validation failures included (`serilog`), and its span wraps the use case (`telemetry`). `ValidationBehaviour` comes next, so invalid requests never reach a handler.

## Steps

Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/optional-token-auth/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ metadata:

Authentication is off until someone sets `Authentication:Authority` (and usually `Authentication:Audience`). Then every request needs a valid JWT bearer token from that authority. The `dotnet new` options `--authority` and `--audience` fill those settings in.

Endpoints always ask for authorization, whether or not authentication is on. When it's off, the default policy lets every request through, so turning it on is a settings change, not a code change. Only health checks, the API reference and the version endpoint (where a template has them) are anonymous. The Lambda template has no token authentication; API Gateway or the caller's IAM role does that job.
Endpoints always ask for authorization, whether or not authentication is on. When it's off, the default policy lets every request through, so turning it on is a settings change, not a code change. Only health checks and the API reference (where a template has them) are anonymous. The version endpoint is protected like everything else; telemetry carries `service.version` for anyone who needs to know what's deployed. The Lambda template has no token authentication; API Gateway or the caller's IAM role does that job.

Settings are read through `AuthenticationSettings.FromConfiguration` (`Enabled` is true when the authority isn't empty).

## Steps

1. A new endpoint or group gets `.RequireAuthorization()` (or the presentation layer's equivalent). That's the default; don't skip it.
2. Only make something anonymous if it has to work without a token: health, the API reference, the version. Say why in a comment.
2. Only make something anonymous if it has to work without a token: health and the API reference. Say why in a comment.
3. When authentication is on, the template tells clients how to get a token: a bearer scheme in the OpenAPI document where a template has one, and the protected resource metadata in MCP.
4. Test both modes: one test with no authority (anonymous works), one with an authority and no token (401).

Expand Down
36 changes: 25 additions & 11 deletions .agents/skills/serilog/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: serilog
description: How logging works with Serilog - registration with AddSerilog, configuration from appsettings, sinks per environment, 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.
description: 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.
metadata:
scope: shared
---
Expand All @@ -14,8 +14,11 @@ Serilog is the logger. Application and Infrastructure code log through `Microsof
| Environment | Sink | Where to look |
| --- | --- | --- |
| `local` | Console, with the output template in `appsettings.local.json` | The terminal |
| `docker` | Seq at `http://seq:5341` | <http://localhost:5341> |
| Lambda, anywhere | Console, as compact JSON | CloudWatch Logs, or `docker compose logs` for the emulator |
| `docker` | Console, and OTLP through the collector to the Aspire dashboard | <http://localhost:18888>, or `docker compose logs` |
| Lambda, anywhere | Console, as compact JSON, and OTLP when the endpoint is set | CloudWatch Logs, or `docker compose logs` for the emulator, and the Aspire dashboard in Docker Compose |
| Any, when `OTEL_EXPORTER_OTLP_ENDPOINT` is set | Also OTLP, through `Serilog.Sinks.OpenTelemetry` | Whatever 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:

Expand All @@ -26,7 +29,7 @@ Every event carries these properties:
| `AssemblyVersion`, `AssemblyInformationalVersion` | Added in code at startup |
| Anything pushed with `LogContext` | `FromLogContext` |

Levels are set in configuration, never in code: `Information` by default and `Debug` in `local`, with `Information` for `Microsoft`, `Warning` for `Microsoft.AspNetCore` and `Error` for `System`. The built-in `Logging` section is set to `None` so nothing is written twice.
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:

Expand All @@ -45,9 +48,9 @@ The Lambda template has no appsettings files. Its startup sets the same levels a
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 Seq without quoting.
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 Seq, filter with property expressions: `ApplicationName = 'CleanModelContextProtocol-Docker' and @Level = 'Error'`.
6. In the Aspire dashboard's structured logs, filter on any property, or open a log's trace from its trace ID.

## Example

Expand All @@ -61,19 +64,30 @@ private static partial void LogHandled(ILogger logger, string requestName, doubl
// src/Presentation/Extensions, where startup registers services
_ = builder.Services.AddSerilog((services, loggerConfiguration) =>
{
var assembly = Assembly.GetEntryAssembly();
var configuration = services.GetRequiredService<IConfiguration>();

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

if (telemetry.Enabled)
{
_ = loggerConfiguration.WriteTo.OpenTelemetry(
options =>
{
options.ResourceAttributes = telemetry.ResourceAttributes;
options.OnBeginSuppressInstrumentation = SuppressInstrumentationScope.Begin;
},
key => configuration[key]);
}
});
```

```json
// src/Presentation/appsettings.docker.json
"Serilog": {
"WriteTo": [ { "Name": "Seq", "Args": { "serverUrl": "http://seq:5341" } } ],
"WriteTo": [ { "Name": "Console", "Args": { "outputTemplate": "..." } } ],
"Properties": { "ApplicationName": "CleanModelContextProtocol-Docker" }
}
```
Expand All @@ -94,6 +108,6 @@ _ = builder.Services.AddSerilog((services, loggerConfiguration) =>

``` bash
dotnet run --project ./src/Presentation # watch the console, then make a request
docker compose up -d --build # then open http://localhost:5341
docker compose up -d --build # then open http://localhost:18888
dotnet test tests/Application.Tests.Unit # LoggingBehaviourTests check the messages with FakeLogger
```
Loading
Loading