MCP Server Logging and OpenTelemetry in ASP.NET Core 10 Using C# – Complete Guide (2026)
A slow MCP call can come from transport negotiation, queueing, or the tool itself. The HTTP status alone cannot tell you which layer failed. An MCP server can return an HTTP success status while the tool response reports an error. Instrument both layers and give operators a way to join evidence without storing tool arguments.
This ASP.NET Core 10 lab emits structured logs, correlated OpenTelemetry traces, and bounded-cardinality tool metrics for a read-only sample tax quote. It exports traces and metrics to a local OTLP collector and writes application logs as JSON.
What should an MCP server measure?
| Signal | What to record | What to avoid |
|---|---|---|
| Logs | Stable operation name, outcome, trace ID | Authorization headers and raw prompts |
| Traces | Time spent inside HTTP and tool operations | Customer data in span names or tags |
| Metrics | Call count and duration with a small set of outcomes | Trace IDs, full paths, or customer IDs as labels |
The incoming HTTP activity is the parent of the application activity if propagation is available. A trace ID identifies the trace, while a span ID identifies one operation inside it. Neither value authenticates the caller. A progress token serves another purpose. It correlates interim protocol notifications with an active request and does not replace trace context.
The example chooses an application-owned ActivitySource and Meter so instrument names are stable even if SDK diagnostics evolve. Record expected invalid input as a distinct outcome. A transport-level 200 should never be your only tool success counter. See OpenTelemetry .NET instrumentation and Build an MCP server in C#.
Create the server
Install the .NET 10 SDK, Docker for the collector, and an MCP client. Create a web project and replace the generated project file and Program.cs. The dependency versions are reproducible lab pins. Review package advisories and use supported patches when deploying.
The demo accepts only loopback-host requests without a browser Origin header and must be run with the loopback binding shown below. It deliberately has no remote authentication. Keep the tax calculation as illustrative arithmetic. Real tax rules are jurisdiction-specific.
Why do the logs and metrics contain different fields?
The log scope includes the trace ID to find one specific execution. The metric labels contain only a fixed tool name and one of three outcomes. A million unique trace IDs in a metric dimension would create a million separate series. Keep the exact subtotal out of all three signals. The diagnostic question is whether the tool completed and how long it took, and neither needs the customer’s input.
The histogram samples time from tool entry through the calculation. It does not include network latency, model deliberation, or request time before the tool handler. To isolate a slow dependency, add a child activity around the asynchronous operation and propagate cancellation. Choose buckets aligned with the service-level objective before using the histogram for percentile alerts.
Run a local collector and make a request
Save the configuration below as collector.yaml. The debug exporter is suitable for a disposable local demonstration. Do not route real customer traffic to it.
Connect a compatible MCP client to http://127.0.0.1:5080/mcp, list tools, then call quote_tax with {"subtotal":25}. The expected text result is 2.00. Try {"subtotal":-1} and confirm the MCP tool reports invalid input. Inspect stdout for JSON log records. The collector should show the HTTP and tool spans plus the counter and histogram after the export interval. Validate the parent relationship by comparing the child’s parent span ID with the HTTP span ID. Check the trace ID in the application log against the tool span. Exporters buffer data. Allow an export interval or shut down gracefully before diagnosing an empty collector.
The C# project compiled with .NET SDK 10.0.100. Docker and end-to-end OTLP export were not exercised in this environment.
Diagnose missing telemetry or noisy production signals
| Symptom | Likely cause | Check |
|---|---|---|
| HTTP spans exist, but the tool span is missing | Source was not registered or the tool was never entered | Check AddSource and the tool result |
| Metrics appear late | Periodic collection | Wait for the export interval |
| OTLP connection fails | Port, protocol, or TLS mismatch | Compare gRPC endpoint and collector receiver |
| Failed tools look healthy | Dashboard counts HTTP statuses only | Filter tool metrics by bounded outcome |
| Logs and traces do not join | Scope or sampling mismatch | Include scopes and check the actual trace ID |
In production, remove the debug exporter. Send OTLP over an authenticated channel, limit telemetry access and retention, and apply collector filtering as a second layer. The first layer is application code that never records the sensitive field. Test filtering with harmless sentinel strings, including inside thrown exceptions, model responses, nested attributes, and exporter failures. Watch for auto-instrumentation that adds headers or URLs you did not expect. Coordinate trace sampling with alerting. Counters can still show an error spike even if the corresponding trace was sampled out.
What is a useful alert?
Start with a sustained increase in error rate or duration, plus a minimum request count. Use separate thresholds for invalid inputs and internal errors. Verify that deployments do not reset your dashboard’s interpretation of cumulative counters. If a dependency is slow, use a representative trace to decide whether the bottleneck is connection acquisition, network I/O, or processing.
Frequently asked questions
Should I export the entire MCP request?
Avoid it by default. Instrument operation names, time, status, and safe counts. Collect a more detailed signal only after a specific privacy and retention review.
Why is ActivitySource.StartActivity nullable?
It can return null when there is no listener for that source. Instrumentation should never make the business operation depend on a trace being sampled or exported.
Can I export JSON console logs to the same backend?
Yes, through a compatible log collection pipeline. This sample leaves logs on stdout and exports traces and metrics over OTLP, so you can see the boundaries explicitly.
Conclusion and references
Start with one tool and verify all three signals against the same call. Use bounded labels for dashboards and trace IDs for investigation. Extend the pattern only where it helps distinguish real failure modes.
Comments
Post a Comment