Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export async function fetchApmErrorContext({
},
logger,
categoryCount: 10,
terms: { 'trace.id': traceId },
fields: ['trace.id'],
});

return logCategories?.categories;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import type {
import { getLogsIndices } from '../../utils/get_logs_indices';
import { getTypedSearch } from '../../utils/get_typed_search';
import { getTotalHits } from '../../utils/get_total_hits';
import { getShouldMatchOrNotExistFilter } from '../../utils/get_should_match_or_not_exist_filter';
import { timeRangeFilter } from '../../utils/dsl_filters';
import { timeRangeFilter, kqlFilter } from '../../utils/dsl_filters';
import { parseDatemath } from '../../utils/time';

export async function getToolHandler({
Expand All @@ -25,7 +24,8 @@ export async function getToolHandler({
index,
start,
end,
terms,
kqlFilter: kuery,
fields,
}: {
core: CoreSetup<
ObservabilityAgentBuilderPluginStartDependencies,
Expand All @@ -36,15 +36,16 @@ export async function getToolHandler({
index?: string;
start: string;
end: string;
terms?: Record<string, string>;
kqlFilter?: string;
fields: string[];
}) {
const logsIndices = index?.split(',') ?? (await getLogsIndices({ core, logger }));
const boolFilters = [
...timeRangeFilter('@timestamp', {
...getShouldMatchOrNotExistFilter(terms),
start: parseDatemath(start),
end: parseDatemath(end, { roundUp: true }),
}),
...kqlFilter(kuery),
{ exists: { field: 'message' } },
];

Expand All @@ -66,15 +67,15 @@ export async function getToolHandler({
boolQuery: { filter: boolFilters, must_not: lowSeverityLogLevels },
logger,
categoryCount: 20,
terms,
fields,
}),
getFilteredLogCategories({
esClient,
logsIndices,
boolQuery: { filter: boolFilters, must: lowSeverityLogLevels },
logger,
categoryCount: 10,
terms,
fields,
}),
]);

Expand All @@ -87,14 +88,14 @@ export async function getFilteredLogCategories({
boolQuery,
logger,
categoryCount,
terms,
fields,
}: {
esClient: IScopedClusterClient;
logsIndices: string[];
boolQuery: QueryDslBoolQuery;
logger: Logger;
categoryCount: number;
terms: Record<string, string> | undefined;
fields: string[];
}) {
const search = getTypedSearch(esClient.asCurrentUser);

Expand Down Expand Up @@ -143,7 +144,7 @@ export async function getFilteredLogCategories({
top_hits: {
size: 1,
_source: false,
fields: ['message', '@timestamp', ...Object.keys(terms ?? {})],
fields: ['message', '@timestamp', ...fields],
sort: {
'@timestamp': { order: 'desc' },
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { timeRangeSchemaOptional, indexDescription } from '../../utils/tool_sche
import { getAgentBuilderResourceAvailability } from '../../utils/get_agent_builder_resource_availability';
import type { getFilteredLogCategories } from './handler';
import { getToolHandler } from './handler';
import { OBSERVABILITY_GET_CORRELATED_LOGS_TOOL_ID } from '../get_correlated_logs/tool';

export interface GetLogCategoriesToolResult {
type: ToolResultType.other;
Expand All @@ -37,11 +38,17 @@ export const OBSERVABILITY_GET_LOG_CATEGORIES_TOOL_ID = 'observability.get_log_c
const getLogsSchema = z.object({
...timeRangeSchemaOptional(DEFAULT_TIME_RANGE),
index: z.string().describe(indexDescription).optional(),
terms: z
.record(z.string(), z.string())
kqlFilter: z
.string()
.optional()
.describe(
'Optional field filters to narrow down results. Each key-value pair filters logs where the field exactly matches the value. Example: { "service.name": "payment", "host.name": "web-server-01" }. Multiple filters are combined with AND logic.'
'A KQL query to filter logs. Examples: service.name:"payment", host.name:"web-server-01", service.name:"payment" AND log.level:error'
),
fields: z
.array(z.string())
.optional()
.describe(
'Additional fields to return for each log sample. "message" and "@timestamp" are always included. Example: ["service.name", "host.name"]'
),
});

Expand Down Expand Up @@ -69,9 +76,15 @@ When to use:
How it works:
Groups similar log messages together using pattern recognition, returning representative categories with counts.

After using this tool:
- For high-count error categories, use \`${OBSERVABILITY_GET_CORRELATED_LOGS_TOOL_ID}\` to trace the sequence of events leading to those errors
- Compare error patterns across services - the origin service often has different error types than affected downstream services (e.g., "constraint violation" vs "connection timeout")
- Patterns like "timeout", "exhausted", "capacity", "limit reached" are often SYMPTOMS - look for what's causing the resource pressure
- If you see resource lifecycle logs (acquire/release, open/close), check if counts match - mismatches can indicate leaks

Do NOT use for:
- Understanding the sequence of events for a specific error (use get_correlated_logs)
- Investigating a specific incident in detail (use get_correlated_logs)
- Understanding the sequence of events for a specific error (use ${OBSERVABILITY_GET_CORRELATED_LOGS_TOOL_ID})
- Investigating a specific incident in detail (use ${OBSERVABILITY_GET_CORRELATED_LOGS_TOOL_ID})
- Analyzing changes in log volume over time (use run_log_rate_analysis)`,
schema: getLogsSchema,
tags: ['observability', 'logs'],
Expand All @@ -86,7 +99,8 @@ Do NOT use for:
index,
start = DEFAULT_TIME_RANGE.start,
end = DEFAULT_TIME_RANGE.end,
terms,
kqlFilter,
fields = [],
} = toolParams;

try {
Expand All @@ -97,7 +111,8 @@ Do NOT use for:
index,
start,
end,
terms,
kqlFilter,
fields,
});

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { timerange } from '@kbn/synthtrace-client';
import { type LogsSynthtraceEsClient, generateLogCategoriesData } from '@kbn/synthtrace';
import { OBSERVABILITY_GET_LOG_CATEGORIES_TOOL_ID } from '@kbn/observability-agent-builder-plugin/server/tools';
import type { GetLogCategoriesToolResult } from '@kbn/observability-agent-builder-plugin/server/tools/get_log_categories/tool';
import { first } from 'lodash';
import type { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context';
import { createAgentBuilderApiClient } from '../utils/agent_builder_client';

Expand Down Expand Up @@ -53,7 +52,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
params: {
start: START,
end: END,
terms: { 'service.name': SERVICE_NAME },
kqlFilter: `service.name:"${SERVICE_NAME}"`,
},
});

Expand Down Expand Up @@ -81,7 +80,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
params: {
start: START,
end: END,
terms: { 'service.name': SERVICE_NAME },
kqlFilter: `service.name:"${SERVICE_NAME}"`,
},
});

Expand Down Expand Up @@ -112,15 +111,13 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
expect(category).to.have.property('count');
expect(category.count).to.be.greaterThan(0);

// Sample should include requested fields
// Sample should include core fields
expect(category.sample).to.have.property('message');
expect(category.sample).to.have.property('@timestamp');
expect(category.sample).to.have.property('service.name');
expect(first(category.sample['service.name'])).to.be(SERVICE_NAME);
});
});

it('works without terms filter', async () => {
it('works without kqlFilter', async () => {
const results = await agentBuilderApiClient.executeTool<GetLogCategoriesToolResult>({
id: OBSERVABILITY_GET_LOG_CATEGORIES_TOOL_ID,
params: {
Expand Down