In a previous post, The Aspire CLI, Revisited, I covered many of the improvements in the Aspire CLI. In this post, I’ll cover a couple of the command line improvements that weren’t covered there: getting at your logs and telemetry from the terminal, and running the Aspire dashboard without an AppHost at all.
The dashboard is one of the best things about Aspire. But there’s a whole category of debugging where a browser is the wrong tool. You’re on a machine you SSH’d into. You’re in a CI log. You want to pipe something into grep or save it to a file. Or, increasingly, you’ve got a coding agent that needs to look at what your app just did, and handing it a web UI isn’t always the best approach.
The dashboard, minus the AppHost
Let’s start with the following scenario: You like the Aspire dashboard, but sometimes you don’t want to wire up a full Aspire application. Turns out you can.
aspire dashboard run starts the Aspire Dashboard in standalone mode, with no AppHost involved at all. It’s there to consume telemetry from anything that emits OTLP: your own services, third-party tools, and applications that have nothing to do with Aspire.
aspire dashboard run
You get back something like this:
PS C:\projects> aspire dashboard run
Dashboard: http://localhost:18888/login?t=6d035b2ae23c7fcfa770d7894cc9ed33
OTLP/gRPC: http://localhost:4317
OTLP/HTTP: http://localhost:4318
Logs: C:\Users\barre\.aspire\logs\cli_20260922T195653_86bc7d95.log
Open the dashboard URL, point any OTLP-compatible application at the gRPC or HTTP endpoint, and your logs, traces, and metrics start showing up live.
Now, running the dashboard standalone isn’t new. What’s new is that it doesn’t take any work. Previously you’d pull the standalone container image and configure ports, certificates, and the OTLP endpoint by hand. The CLI now handles all of that using the same dashboard binary that ships with the SDK. And it doesn’t need Docker or any other container runtime to do it. The container image is still there if you’d rather, or if you’re in an environment where running the CLI isn’t an option.
The command is interactive and blocking, so it holds the foreground while the dashboard is running.
Poof! You now have a zero-setup, locally-running OTLP collector with a decent UI, one command away, on any machine with the Aspire CLI on it. Your app doesn’t have to be an Aspire app. It doesn’t have to be .NET. If it speaks OTLP, this works. This makes aspire dashboard run useful to people who aren’t using Aspire at all.
Note that this one is still in preview.
Searching console logs
Back inside a running app, the simplest thing you can do is look at console logs for a resource:
aspire logs redis --search "timeout"
Console log search matches against the log line text and the resource name. That’s it — free text only, no structured qualifiers. Which is fine, because console logs aren’t structured either.
You can follow the stream while filtering it, which is where this starts being genuinely useful:
aspire logs --follow --search "\"connection error\""
Note the escaped inner quotes. Wrapping a phrase in quotes inside the search string makes it a single fragment rather than two separate words that both have to appear somewhere.
Structured logs, traces, and spans
The aspire otel commands are where the real filtering lives. There are three of them:
aspire otel logs— structured logsaspire otel traces— tracesaspire otel spans— individual spans
All three accept --search, and all three support a lot more than free text.
The search syntax
The same syntax works across all four search-capable commands — the three above plus aspire logs — with each one exposing a different set of fields. The one caveat is the one I mentioned earlier: console logs only support the free-text parts of what follows. Everything from the field qualifiers down applies to the otel commands only.
| Syntax | What it does | Example |
|---|---|---|
word | Free-text fragment. Must appear in at least one searchable field | timeout |
"quoted phrase" | A single fragment containing spaces | "connection refused" |
field:value | Field qualifier. Must match that named field | severity:error |
field:"value with spaces" | Quoted field qualifier value | message:"connection failed" |
@attr:value | Attribute qualifier. Matches custom span and log attributes | @http.method:GET |
-field:value | Negation. Excludes matches | -severity:debug |
field:>value | Comparison, for numeric and timestamp fields. Also >=, <, <= | duration:>100 |
The critical detail: every term is AND’d. Each fragment and each qualifier has to match independently. There’s no OR. If you’re used to writing log queries with boolean expressions, that’s an adjustment. You narrow by adding terms, but you can’t express “either of these two things.” It’s always “all the things.”
The @ prefix is the one I’d highlight. That’s how you reach into custom attributes on spans and logs, which is where most of the interesting application-specific data actually lives:
aspire otel traces --search "@http.status_code:500"
aspire otel spans --search "@db.system:postgresql"
If you’ve been diligent about adding attributes to your spans, this is the payoff. If you haven’t, this is the argument for starting.
What each command can filter on
Free text searches a different set of fields depending on which signal you’re querying:
- Structured logs match against message, resource name, scope, trace and span IDs, severity, and all attribute keys and values. Field qualifiers:
severity,resource,scope,message,trace-id,span-id,event,timestamp. - Traces match against trace name, resource name, and all span fields within the trace. Field qualifiers:
name,resource,trace-id,status,duration,timestamp. - Spans match against span name, resource name, scope, trace and span IDs, status, kind, and all attribute keys and values. Field qualifiers:
name,resource,scope,status,kind,trace-id,span-id,duration,timestamp.
And a few queries that show what you can actually build with this:
# Errors from one resource, minus the noise
aspire otel logs --search "resource:api -severity:debug"
# Everything EF Core said
aspire otel logs --search "scope:Microsoft.EntityFrameworkCore"
# Failed requests that were also slow
aspire otel traces --search "status:error duration:>500"
# Slow outbound GETs that failed, excluding internal spans
aspire otel spans --search "@http.method:GET duration:>100 status:error -kind:internal"
That last one is four qualifiers in a single string, and it reads about as well as anything you’d type into a log aggregator’s query box.
The timestamp qualifier, and one gotcha
timestamp works on structured logs, traces, and spans, and takes ISO 8601 dates or date/times with a comparison operator:
aspire otel logs --search "timestamp:>2026-01-15T09:30:00"
aspire otel traces --search "timestamp:<=2026-06-01T12:00:00Z"
aspire otel spans --search "timestamp:>=2026-01-01 timestamp:<2027-01-01"
Since everything is AND’d, two timestamp qualifiers give you a range, as in that third example. Remember, though, that timestamps are stored internally in UTC, and how your value gets converted depends on how you wrote it:
- No timezone suffix — treated as local time on the dashboard server, then converted to UTC.
- A UTC offset like
+05:00— adjusted to UTC. - A
Zsuffix — treated as UTC directly.
That first rule is the most likely to catch you out. It’s the dashboard server’s local time, not yours. On your own machine those are the same thing and you’ll never notice. Pointed at a dashboard running somewhere else, they’re not, and you’ll get a window that’s off by however many hours separate you. When in doubt, use Z or +00:00 and do the math yourself.
Pointing the CLI at a standalone dashboard
The two halves of this post connect. aspire otel logs and aspire otel traces take --dashboard-url and --api-key, so you can query a standalone dashboard without an AppHost running anywhere:
# Stream structured logs
aspire otel logs --dashboard-url https://localhost:18888/login?t=TOKEN -f
# Search recent traces
aspire otel traces --dashboard-url https://localhost:18888/login?t=TOKEN
--dashboard-url takes either the dashboard’s base URL or the full login URL. It normalizes the login form automatically, so you can paste whatever aspire dashboard run printed at you without editing it first.
So the full workflow is:
- Start a standalone dashboard
- Point your app at the OTLP endpoint
- Query the results from a second terminal without ever opening the browser. Or open it. Both work, and they’re looking at the same data.
Why the filtering happens where it does
One implementation detail is worth calling out, because it changes how you should use this.
Search, tail, and resource filters are applied before the data is streamed back to the CLI. You’re not pulling down a firehose and filtering it locally. The narrowing happens upstream, which keeps things responsive on log streams big enough that local filtering would crawl.
That’s nice for you. Where it really pays off is for a coding agent.
If you’ve read the agent posts on this blog, you know the recurring problem: context is finite and expensive. An agent debugging your app by running aspire logs and grepping the output has to pull every line into its context window first, and then think about it. An agent that can express the filter in the command gets back only the matching lines. That’s faster, cheaper, and with a much better signal-to-noise ratio in what it actually reads.
The Aspire team called this out explicitly as a design goal, and it shows up in the agent skills I mentioned last time. aspire-monitoring exists precisely so an agent knows how to investigate telemetry this way rather than by brute force.
It’s a good example of a thing I keep running into: tools built for humans and tools built for agents are converging. The features that make one better usually make the other better too. A filter that saves you from scrolling saves an agent from burning tokens.
Where that leaves us
Between aspire dashboard run and the otel commands, most of what you used to need the dashboard UI for is now available from a terminal. Not all of it — the trace waterfall view is genuinely better as a picture, and I’m not going to pretend otherwise. But for “what happened in the last five minutes, filtered to the thing I care about,” the CLI is now the faster path.
If you want to go deeper on the telemetry side of this, the earlier post in this series on enhancing telemetry in Aspire covers what you should be emitting in the first place. All the filtering in the world doesn’t help if the attributes aren’t there.

