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 |
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):
This is good — clean and useful.
Proxy error (backend down):
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
One structured line: error message, request, status, duration.
Proposed improvements
1. Request logging middleware
A
Log.Levelconfig option that controls our own request logging:defaultrequestsdebugExample verbose output:
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=debugshould print the resolvedYarpAppConfigobject as JSON at startup, so operators can verify what the app is actually running with.4. Preserve escape hatch
The standard
Logging:LogLevelandLogging:Console:LogLevelconfig sections continue to work for fine-grained control. OurLog.Levelis the opinionated layer on top.Tested scenarios
These were tested with an actual Caddy comparison using a Node.js backend at
localhost:5100and broken routes pointing atlocalhost:9999andwrong-hostname:8080. The sample is at/tmp/yarp-spa-sample/.Log.Level=requestsLog.Level=debug