Skip to content

Improve logging and error output in the YARP container image #3016

@davidfowl

Description

@davidfowl

Context

The YARP container image startup and error output is raw ASP.NET Core framework logging. Compared to tools like Caddy and nginx, the experience is noisy and hard to troubleshoot.

What we have today

Startup (after #3015 work):

YARP

  Config:         /app/config.json
  Static files:   /app/wwwroot
  SPA fallback:   /index.html

  Listening on:   http://localhost:5999

This is good — clean and useful.

Proxy error (backend down):

warn: Yarp.ReverseProxy.Forwarder.HttpForwarder[48]
      Request: An error was encountered before receiving a response.
      System.Net.Http.HttpRequestException: Connection refused (localhost:5100)
       ---> System.Net.Sockets.SocketException (61): Connection refused
         at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs...
         at System.Net.Http.HttpConnectionPool.ConnectToTcpHostAsync...
         ...15 more lines...

This is bad — 20 lines of stack trace per failed request. No route name, no request URI, no status code, no duration.

What Caddy shows for the same error

ERROR  http.log.error  dial tcp [::1]:5100: connect: connection refused
  {"request": {"method": "GET", "uri": "/api/users"}, "status": 502, "duration": 0.001943}

One structured line: error message, request, status, duration.

Proposed improvements

1. Request logging middleware

A Log.Level config option that controls our own request logging:

Level Behavior
default Banner + warnings/errors only. No per-request output.
requests Single-line per request: method, path, what handled it, status, duration
debug Full framework logs + resolved config dump at startup

Example verbose output:

GET  /                        → static   200  510b   3ms
GET  /_astro/app.f4e2d1.css   → static   200  224b   1ms
GET  /api/users               → proxy    200         45ms
GET  /dashboard               → fallback 200  510b   2ms
GET  /missing.js              →          404          0ms
GET  /api/users               → proxy    502         74ms  Connection refused (localhost:5100)

2. Condensed error output

Proxy errors should be single-line on console by default, not full stack traces. The root cause message (Connection refused, no such host) is what matters — not the .NET call stack.

Include: route name, destination address, request path, status code, error summary.

3. Config dump in debug mode

Log.Level=debug should print the resolved YarpAppConfig object as JSON at startup, so operators can verify what the app is actually running with.

4. Preserve escape hatch

The standard Logging:LogLevel and Logging:Console:LogLevel config sections continue to work for fine-grained control. Our Log.Level is the opinionated layer on top.

Tested scenarios

These were tested with an actual Caddy comparison using a Node.js backend at localhost:5100 and broken routes pointing at localhost:9999 and wrong-hostname:8080. The sample is at /tmp/yarp-spa-sample/.

Scenario YARP today Caddy Goal
Startup Clean banner ✅ Structured JSON startup Keep our banner
Successful request Silent ✅ Silent (unless access log on) Keep silent by default
Proxy error 20-line stack trace 1-line structured JSON 1-line summary
Verbose/access log Not available Via access log config Log.Level=requests
Debug/config dump Not available N/A Log.Level=debug

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions