Skip to content
Open
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
41 changes: 40 additions & 1 deletion .agents/skills/aspire/references/monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ aspire otel traces [resource] --format Json
aspire otel spans [resource] --format Json
aspire otel logs --trace-id <id> --format Json
aspire otel logs [resource] --search "connection timeout"
aspire otel spans [resource] --search "/api/products"
aspire otel traces [resource] --search "/api/products"
aspire otel logs [resource] --search "severity:error"
aspire otel spans [resource] --search "@http.method:GET duration:>100"
aspire logs [resource]
aspire logs [resource] --search "error"
```
Expand All @@ -44,6 +46,43 @@ Keep these points in mind:
- `[resource]` is optional. Include it to filter results to a single resource; omit it to see all resources.
- `--search` can be combined with other options like `--format Json`, `--trace-id`, `--limit`, and resource filtering.

## Filtering

The `--search` option filters output by matching text against log/span content.

- Multiple words are AND'd — all fragments must match.
- Use `"quoted phrases"` for multi-word fragments: `--search "\"connection timeout\""`.
- Qualifiers support quoted values: `--search "message:\"connection failed\""`.

### Console Logs (`aspire logs --search`)

Matches against log line text content and resource name. Only free-text is supported (no structured qualifiers).

```bash
aspire logs redis --search "timeout"
aspire logs --follow --search "\"connection error\""
```

### Structured Telemetry (`aspire otel logs/traces/spans --search`)

Supports free-text and structured qualifiers in a single query.

**Free-text** matches against message, resource name, scope, trace/span IDs, severity/status, and all attribute keys/values.

**Structured qualifiers** filter on specific fields with `key:value` syntax:

- Log keys: `severity`, `resource`, `scope`, `message`, `trace-id`, `span-id`, `event`
- Span/Trace keys: `name`, `resource`, `scope`, `status`, `kind`, `trace-id`, `span-id`, `duration`
- Custom attributes: `@http.method:GET`, `@db.system:redis`
- Negation: `-severity:debug`, `-@db.system:redis`
- Comparison: `duration:>100`, `duration:>=50`

```bash
aspire otel logs --search "severity:error \"connection failed\""
aspire otel spans --search "@http.method:GET duration:>100 status:error"
aspire otel logs --search "resource:api -severity:debug"
```

## Scenario: I Need A Sharable Diagnostics Bundle

Use this command when you need a portable handoff artifact for deeper analysis or for another person to inspect offline.
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Cli/Aspire.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
<Compile Include="$(SharedDir)CircularBuffer.cs" Link="Utils\CircularBuffer.cs" />
<Compile Include="$(SharedDir)ColorGenerator.cs" Link="Utils\ColorGenerator.cs" />
<Compile Include="$(SharedDir)StringComparers.cs" Link="Utils\StringComparers.cs" />
<Compile Include="$(SharedDir)SearchTextParser.cs" Link="Utils\SearchTextParser.cs" />
<Compile Include="$(SharedDir)KnownUnsupportedUrlSchemes.cs" Link="Utils\KnownUnsupportedUrlSchemes.cs" />
<Compile Include="$(SharedDir)LocaleHelpers.cs" Link="Utils\LocaleHelpers.cs" />
<Compile Include="$(SharedDir)DurationFormatter.cs" Link="Utils\DurationFormatter.cs" />
Expand Down
17 changes: 14 additions & 3 deletions src/Aspire.Cli/Commands/LogsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -485,11 +485,22 @@ private static string FormatTimestamp(DateTime timestamp)

private static bool MatchesSearch(LogEntry entry, string search)
{
var fragments = SearchTextParser.ParseFragments(search);
if (fragments.Length == 0)
{
return true;
}

var content = entry.RawContent ?? entry.Content ?? string.Empty;
var prefix = entry.ResourcePrefix ?? string.Empty;
return content.Contains(search, StringComparisons.FullTextSearch) ||
prefix.Contains(search, StringComparisons.FullTextSearch) ||
AnsiParser.StripControlSequences(content).Contains(search, StringComparisons.FullTextSearch);
var stripped = AnsiParser.StripControlSequences(content);

// Console logs have no structured attributes, so all search text is treated as
// free-text fragments matched against the log content and resource name.
return SearchTextParser.MatchesAllFragments(fragments, (content, prefix, stripped), static (state, fragment) =>
state.content.Contains(fragment, StringComparisons.FullTextSearch) ||
state.prefix.Contains(fragment, StringComparisons.FullTextSearch) ||
state.stripped.Contains(fragment, StringComparisons.FullTextSearch));
}

private static string ResolveResourceName(string resourceName, IEnumerable<ResourceSnapshot> snapshots)
Expand Down
8 changes: 0 additions & 8 deletions src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,6 @@ internal static class TelemetryCommandHelpers
Description = TelemetryCommandStrings.SearchOptionDescription
};

/// <summary>
/// Minimum span duration option for spans and traces commands.
/// </summary>
internal static Option<double?> CreateMinimumDurationOption() => new("--min-duration", "--min-duration-ms")
{
Description = TelemetryCommandStrings.MinimumDurationOptionDescription
};

/// <summary>
/// Dashboard URL option for connecting directly to a standalone dashboard.
/// </summary>
Expand Down
8 changes: 2 additions & 6 deletions src/Aspire.Cli/Commands/TelemetrySpansCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ internal sealed class TelemetrySpansCommand : BaseCommand
private static readonly Option<string?> s_dashboardUrlOption = TelemetryCommandHelpers.CreateDashboardUrlOption();
private static readonly Option<string?> s_apiKeyOption = TelemetryCommandHelpers.CreateApiKeyOption();
private static readonly Option<string?> s_searchOption = TelemetryCommandHelpers.CreateSearchOption();
private static readonly Option<double?> s_minimumDurationOption = TelemetryCommandHelpers.CreateMinimumDurationOption();

public TelemetrySpansCommand(
IInteractionService interactionService,
Expand Down Expand Up @@ -75,7 +74,6 @@ public TelemetrySpansCommand(
Options.Add(s_dashboardUrlOption);
Options.Add(s_apiKeyOption);
Options.Add(s_searchOption);
Options.Add(s_minimumDurationOption);
}

protected override async Task<CommandResult> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
Expand All @@ -92,7 +90,6 @@ protected override async Task<CommandResult> ExecuteAsync(ParseResult parseResul
var dashboardUrl = parseResult.GetValue(s_dashboardUrlOption);
var apiKey = parseResult.GetValue(s_apiKeyOption);
var search = parseResult.GetValue(s_searchOption);
var minimumDuration = parseResult.GetValue(s_minimumDurationOption);

// Validate --limit value
if (limit.HasValue && limit.Value < 1)
Expand All @@ -108,7 +105,7 @@ protected override async Task<CommandResult> ExecuteAsync(ParseResult parseResul
return CommandResult.FromExitCode(dashboardApi.ExitCode);
}

return CommandResult.FromExitCode(await FetchSpansAsync(dashboardApi.BaseUrl!, dashboardApi.ApiToken!, resourceName, traceId, hasError, limit, follow, format, dashboardOnly: dashboardUrl is not null, dashboardApi.DashboardUrl!, search, minimumDuration, cancellationToken));
return CommandResult.FromExitCode(await FetchSpansAsync(dashboardApi.BaseUrl!, dashboardApi.ApiToken!, resourceName, traceId, hasError, limit, follow, format, dashboardOnly: dashboardUrl is not null, dashboardApi.DashboardUrl!, search, cancellationToken));
}

private async Task<int> FetchSpansAsync(
Expand All @@ -123,7 +120,6 @@ private async Task<int> FetchSpansAsync(
bool dashboardOnly,
string dashboardUrl,
string? search,
double? minimumDuration,
CancellationToken cancellationToken)
{
try
Expand All @@ -147,7 +143,7 @@ private async Task<int> FetchSpansAsync(
// Build URL with query parameters
int? effectiveLimit = (limit.HasValue && !follow) ? limit.Value : null;

var url = DashboardUrls.TelemetrySpansApiUrl(baseUrl, resolvedResources, traceId: traceId, hasError: hasError, limit: effectiveLimit, follow: follow ? true : null, search: search, minDurationMs: minimumDuration);
var url = DashboardUrls.TelemetrySpansApiUrl(baseUrl, resolvedResources, traceId: traceId, hasError: hasError, limit: effectiveLimit, follow: follow ? true : null, search: search);

_logger.LogDebug("Fetching spans from {Url}", url);

Expand Down
13 changes: 4 additions & 9 deletions src/Aspire.Cli/Commands/TelemetryTracesCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ internal sealed class TelemetryTracesCommand : BaseCommand
private static readonly Option<string?> s_dashboardUrlOption = TelemetryCommandHelpers.CreateDashboardUrlOption();
private static readonly Option<string?> s_apiKeyOption = TelemetryCommandHelpers.CreateApiKeyOption();
private static readonly Option<string?> s_searchOption = TelemetryCommandHelpers.CreateSearchOption();
private static readonly Option<double?> s_minimumDurationOption = TelemetryCommandHelpers.CreateMinimumDurationOption();

public TelemetryTracesCommand(
IInteractionService interactionService,
Expand Down Expand Up @@ -74,7 +73,6 @@ public TelemetryTracesCommand(
Options.Add(s_dashboardUrlOption);
Options.Add(s_apiKeyOption);
Options.Add(s_searchOption);
Options.Add(s_minimumDurationOption);
}

protected override async Task<CommandResult> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
Expand All @@ -90,7 +88,6 @@ protected override async Task<CommandResult> ExecuteAsync(ParseResult parseResul
var dashboardUrl = parseResult.GetValue(s_dashboardUrlOption);
var apiKey = parseResult.GetValue(s_apiKeyOption);
var search = parseResult.GetValue(s_searchOption);
var minimumDuration = parseResult.GetValue(s_minimumDurationOption);

// Validate --limit value
if (limit.HasValue && limit.Value < 1)
Expand All @@ -110,11 +107,11 @@ protected override async Task<CommandResult> ExecuteAsync(ParseResult parseResul
{
if (!string.IsNullOrEmpty(traceId))
{
return CommandResult.FromExitCode(await FetchSingleTraceAsync(dashboardApi.BaseUrl!, dashboardApi.ApiToken!, traceId, format, dashboardApi.DashboardUrl!, minimumDuration, cancellationToken));
return CommandResult.FromExitCode(await FetchSingleTraceAsync(dashboardApi.BaseUrl!, dashboardApi.ApiToken!, traceId, format, dashboardApi.DashboardUrl!, cancellationToken));
}
else
{
return CommandResult.FromExitCode(await FetchTracesAsync(dashboardApi.BaseUrl!, dashboardApi.ApiToken!, resourceName, hasError, limit, format, dashboardApi.DashboardUrl!, search, minimumDuration, cancellationToken));
return CommandResult.FromExitCode(await FetchTracesAsync(dashboardApi.BaseUrl!, dashboardApi.ApiToken!, resourceName, hasError, limit, format, dashboardApi.DashboardUrl!, search, cancellationToken));
}
}
catch (HttpRequestException ex)
Expand All @@ -132,7 +129,6 @@ private async Task<int> FetchSingleTraceAsync(
string traceId,
OutputFormat format,
string dashboardUrl,
double? minimumDuration,
CancellationToken cancellationToken)
{
using var client = TelemetryCommandHelpers.CreateApiClient(_httpClientFactory, apiToken);
Expand All @@ -144,7 +140,7 @@ private async Task<int> FetchSingleTraceAsync(
// Pre-resolve colors so assignment is deterministic regardless of data order
TelemetryCommandHelpers.ResolveResourceColors(_resourceColorMap, allOtlpResources);

var url = DashboardUrls.TelemetryTraceDetailApiUrl(baseUrl, traceId, minDurationMs: minimumDuration);
var url = DashboardUrls.TelemetryTraceDetailApiUrl(baseUrl, traceId);

_logger.LogDebug("Fetching trace {TraceId} from {Url}", traceId, url);

Expand Down Expand Up @@ -194,7 +190,6 @@ private async Task<int> FetchTracesAsync(
OutputFormat format,
string dashboardUrl,
string? search,
double? minimumDuration,
CancellationToken cancellationToken)
{
using var client = TelemetryCommandHelpers.CreateApiClient(_httpClientFactory, apiToken);
Expand All @@ -214,7 +209,7 @@ private async Task<int> FetchTracesAsync(
// Pre-resolve colors so assignment is deterministic regardless of data order
TelemetryCommandHelpers.ResolveResourceColors(_resourceColorMap, allOtlpResources);

var url = DashboardUrls.TelemetryTracesApiUrl(baseUrl, resolvedResources, hasError: hasError, limit: limit, search: search, minDurationMs: minimumDuration);
var url = DashboardUrls.TelemetryTracesApiUrl(baseUrl, resolvedResources, hasError: hasError, limit: limit, search: search);

_logger.LogDebug("Fetching traces from {Url}", url);

Expand Down
20 changes: 15 additions & 5 deletions src/Aspire.Cli/Mcp/Tools/ListConsoleLogsTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,23 @@ public override async ValueTask<CallToolResult> CallToolAsync(CallToolContext co

var entries = logEntries.GetEntries().ToList();

// Apply full-text search filter on log content
// Console logs have no structured attributes, so all search text is treated as
// free-text fragments matched against the log content and resource name.
if (!string.IsNullOrEmpty(search))
{
entries = entries.Where(e =>
(e.Content is not null && e.Content.Contains(search, StringComparisons.FullTextSearch)) ||
(e.RawContent is not null && e.RawContent.Contains(search, StringComparisons.FullTextSearch)))
.ToList();
var fragments = SearchTextParser.ParseFragments(search);
if (fragments.Length > 0)
{
entries = entries.Where(e =>
SearchTextParser.MatchesAllFragments(
fragments,
(e.Content ?? string.Empty, e.RawContent ?? string.Empty, e.ResourcePrefix ?? string.Empty),
static (state, fragment) =>
state.Item1.Contains(fragment, StringComparisons.FullTextSearch) ||
state.Item2.Contains(fragment, StringComparisons.FullTextSearch) ||
state.Item3.Contains(fragment, StringComparisons.FullTextSearch)))
.ToList();
}
}

// When search is applied, total reflects matching entries. Otherwise, use the
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions src/Aspire.Cli/Resources/TelemetryCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,6 @@
<data name="SearchOptionDescription" xml:space="preserve">
<value>Full-text search across telemetry text fields, such as log messages, attribute values, names, source, and IDs</value>
</data>
<data name="MinimumDurationOptionDescription" xml:space="preserve">
<value>Filter by minimum span duration in milliseconds</value>
</data>
<data name="LimitMustBePositive" xml:space="preserve">
<value>The --limit value must be a positive number.</value>
</data>
Expand Down
5 changes: 0 additions & 5 deletions src/Aspire.Cli/Resources/xlf/TelemetryCommandStrings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions src/Aspire.Cli/Resources/xlf/TelemetryCommandStrings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions src/Aspire.Cli/Resources/xlf/TelemetryCommandStrings.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions src/Aspire.Cli/Resources/xlf/TelemetryCommandStrings.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions src/Aspire.Cli/Resources/xlf/TelemetryCommandStrings.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions src/Aspire.Cli/Resources/xlf/TelemetryCommandStrings.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions src/Aspire.Cli/Resources/xlf/TelemetryCommandStrings.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading