This is an automated email from the ASF dual-hosted git repository. zqr10159 pushed a commit to branch 2.0.0 in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
commit 8b72b8a0ff5908238843dd9ad654918eaff63567 Author: Logic <[email protected]> AuthorDate: Sat Jun 6 18:33:37 2026 +0800 feat(observability): preserve signal alert filters --- .../calculate/realtime/window/LogWorkerTest.java | 14 ++ .../alert/service/DataSourceServiceTest.java | 20 ++ web-next/lib/log-manage/view-model.test.ts | 32 ++++ web-next/lib/log-manage/view-model.ts | 16 +- web-next/lib/trace-manage/view-model.test.ts | 106 ++++++++++- web-next/lib/trace-manage/view-model.ts | 208 ++++++++++++++++++++- 6 files changed, 385 insertions(+), 11 deletions(-) diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/LogWorkerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/LogWorkerTest.java index 698023721e..e0eb7e317b 100644 --- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/LogWorkerTest.java +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/LogWorkerTest.java @@ -285,4 +285,18 @@ class LogWorkerTest { "log.traceId == 'trace-123' && log.spanId != 'span-other'", false)); } + + @Test + void testLogExpressionCanReadSeverityNumber() { + LogEntry detailedLogEntry = LogEntry.builder() + .severityNumber(17) + .build(); + + JexlExprCalculator calculator = new JexlExprCalculator(); + + assertTrue(calculator.execAlertExpression( + Map.of("log", detailedLogEntry), + "log.severityNumber == 17", + false)); + } } diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java index c959d6b99b..317c2956e1 100644 --- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java @@ -812,6 +812,26 @@ class DataSourceServiceTest { verify(mockExecutor, Mockito.times(2)).execute(anyString()); } + @Test + void queryTraceScopeAllowsRawTraceJsonResourceFilters() { + List<Map<String, Object>> sqlData = List.of(new HashMap<>(Map.of("__value__", 1))); + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + when(mockExecutor.support("sql")).thenReturn(true); + when(mockExecutor.execute(anyString())).thenReturn(sqlData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.query("sql", + "SELECT service_name, span_name AS operation, span_kind, COUNT(*) AS __value__ FROM hzb_traces " + + "WHERE service_name = 'checkout' " + + "AND json_get_string(resource_attributes, '$[\"service.version\"]') = '1.2.3' " + + "AND span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') " + + "GROUP BY service_name, span_name, span_kind HAVING __value__ > 0", + TRACE_ALERT_THRESHOLD_TYPE_PERIODIC); + + assertEquals(1, result.size()); + verify(mockExecutor).execute(anyString()); + } + @Test void queryLogScopeRejectsTraceTables() { QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); diff --git a/web-next/lib/log-manage/view-model.test.ts b/web-next/lib/log-manage/view-model.test.ts index b26944dc83..70ea819e3f 100644 --- a/web-next/lib/log-manage/view-model.test.ts +++ b/web-next/lib/log-manage/view-model.test.ts @@ -550,6 +550,38 @@ describe('log view model', () => { }); }); + it('keeps severity number scope in executable log alert expressions', () => { + expect(buildLogAlertRuleDraft( + { + search: '', + logContent: '', + traceId: 'trace-123', + spanId: '', + severityNumber: '17', + severityText: '', + resourceFilter: '', + attributeFilter: '' + } as any + )).toMatchObject({ + query: 'severityNumber=17\ntraceId=trace-123', + expression: "log.severityNumber == 17 && log.traceId == 'trace-123'", + template: 'Log matched: {{log.body}}' + }); + }); + + it('suppresses executable log alert expressions for unsafe severity number values', () => { + expect(buildLogAlertRuleDraft({ + search: 'checkout failed', + logContent: '', + traceId: '', + spanId: '', + severityNumber: '17 || true', + severityText: '', + resourceFilter: '', + attributeFilter: '' + } as any).expression).toBeUndefined(); + }); + it('suppresses executable log alert expressions for unsafe trace scope values', () => { expect(buildLogAlertRuleDraft({ search: 'checkout failed', diff --git a/web-next/lib/log-manage/view-model.ts b/web-next/lib/log-manage/view-model.ts index b7bae2c6b9..ef5f21e80d 100644 --- a/web-next/lib/log-manage/view-model.ts +++ b/web-next/lib/log-manage/view-model.ts @@ -398,6 +398,13 @@ function buildLogSeverityAlertExpression(severityText: string | null | undefined return `log.severityText == '${normalized}'`; } +function buildLogSeverityNumberAlertExpression(severityNumber: string | null | undefined) { + const normalized = severityNumber?.trim(); + if (!normalized) return undefined; + if (!/^\d+$/.test(normalized)) return null; + return `log.severityNumber == ${Number(normalized)}`; +} + function escapeLogAlertStringLiteral(value: string) { return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); } @@ -464,6 +471,7 @@ function buildLogFilterAlertExpressions(source: 'resource' | 'attributes', filte export function buildLogAlertRuleDraft(query: LogQueryState, routeContext: SignalRouteContext = {}): SignalAlertRuleDraftContext { const severityExpression = buildLogSeverityAlertExpression(query.severityText); + const severityNumberExpression = buildLogSeverityNumberAlertExpression(query.severityNumber); const contentExpression = buildLogContentAlertExpression(query.logContent || query.search); const resourceExpressions = buildLogFilterAlertExpressions('resource', query.resourceFilter); const attributeExpressions = buildLogFilterAlertExpressions('attributes', query.attributeFilter); @@ -495,9 +503,15 @@ export function buildLogAlertRuleDraft(query: LogQueryState, routeContext: Signa buildLogDirectFieldAlertExpression('spanId', query.spanId || routeContext.spanId) ]; const hasUnsafeTraceScope = traceScopeExpressions.some(expression => expression === null); - const expression = severityExpression !== null && contentExpression !== null && resourceExpressions && attributeExpressions && !hasUnsafeTraceScope + const expression = severityExpression !== null + && severityNumberExpression !== null + && contentExpression !== null + && resourceExpressions + && attributeExpressions + && !hasUnsafeTraceScope ? Array.from(new Set([ severityExpression, + severityNumberExpression, contentExpression, ...routeContextExpressions, ...traceScopeExpressions, diff --git a/web-next/lib/trace-manage/view-model.test.ts b/web-next/lib/trace-manage/view-model.test.ts index cb5c4f4147..9620eae9eb 100644 --- a/web-next/lib/trace-manage/view-model.test.ts +++ b/web-next/lib/trace-manage/view-model.test.ts @@ -391,7 +391,7 @@ describe('trace view model', () => { serviceName: 'checkout', resourceFilter: 'deployment.environment.name="prod"', operationName: 'POST /checkout', - minDurationMs: '250', + minDurationMs: '', maxDurationMs: '', errorOnly: true, spanScope: 'root' @@ -584,7 +584,7 @@ describe('trace view model', () => { resourceFilter: '', operationName: 'POST /checkout', minDurationMs: '250', - maxDurationMs: '', + maxDurationMs: '500', errorOnly: false, spanScope: 'root' } as any, @@ -604,7 +604,75 @@ describe('trace view model', () => { expect(draft.expression).toContain("AND entity_id = '7'"); expect(draft.expression).toContain("AND service_namespace = 'payments'"); expect(draft.expression).toContain("AND deployment_environment = 'prod'"); - expect(draft.expression).toContain('HAVING __value__ >= 250'); + expect(draft.expression).toContain('HAVING __value__ >= 250 AND __value__ <= 500'); + }); + + it('keeps RED-backed resource filters in trace alert SQL', () => { + const draft = buildTraceAlertRuleDraft({ + traceId: '', + spanId: '', + serviceName: '', + resourceFilter: + 'service.name=checkout and service.namespace="payments" and deployment.environment.name=prod and hertzbeat.entity_id=7', + operationName: 'POST /checkout', + minDurationMs: '', + maxDurationMs: '', + errorOnly: true, + spanScope: 'root' + } as any); + + expect(draft.datasource).toBe('sql'); + expect(draft.expression).toContain("WHERE service_name = 'checkout'"); + expect(draft.expression).toContain("AND operation = 'POST /checkout'"); + expect(draft.expression).toContain("AND service_namespace = 'payments'"); + expect(draft.expression).toContain("AND deployment_environment = 'prod'"); + expect(draft.expression).toContain("AND entity_id = '7'"); + expect(draft.expression).toContain('HAVING __value__ > 0'); + }); + + it('falls back to raw trace SQL for non-RED resource filters', () => { + const draft = buildTraceAlertRuleDraft({ + traceId: '', + spanId: '', + serviceName: 'checkout', + resourceFilter: 'service.version=1.2.3', + operationName: 'POST /checkout', + minDurationMs: '', + maxDurationMs: '', + errorOnly: true, + spanScope: 'root' + } as any); + + expect(draft.datasource).toBe('sql'); + expect(draft.expression).toContain('FROM hzb_traces'); + expect(draft.expression).toContain("WHERE service_name = 'checkout'"); + expect(draft.expression).toContain("AND span_name = 'POST /checkout'"); + expect(draft.expression).toContain("AND json_get_string(resource_attributes, '$[\"service.version\"]') = '1.2.3'"); + expect(draft.expression).toContain("AND span_status_code IN ('STATUS_CODE_ERROR', 'ERROR')"); + expect(draft.expression).toContain("AND (parent_span_id IS NULL OR parent_span_id = '')"); + expect(draft.expression).toContain('COUNT(*) AS __value__'); + expect(draft.expression).toContain('HAVING __value__ > 0'); + }); + + it('uses raw trace SQL for error-only alerts with duration filters', () => { + const draft = buildTraceAlertRuleDraft({ + traceId: '', + spanId: '', + serviceName: 'checkout', + resourceFilter: '', + operationName: '', + minDurationMs: '250', + maxDurationMs: '500', + errorOnly: true, + spanScope: 'root' + } as any); + + expect(draft.datasource).toBe('sql'); + expect(draft.expression).toContain('FROM hzb_traces'); + expect(draft.expression).toContain('duration_nano >= 250000000'); + expect(draft.expression).toContain('duration_nano <= 500000000'); + expect(draft.expression).toContain("span_status_code IN ('STATUS_CODE_ERROR', 'ERROR')"); + expect(draft.expression).toContain('HAVING __value__ > 0'); }); it('does not invent trace latency SQL from unsafe duration or optional filters', () => { @@ -626,6 +694,38 @@ describe('trace view model', () => { errorOnly: false, spanScope: 'root' } as any).expression).toBeUndefined(); + expect(buildTraceAlertRuleDraft({ + traceId: '', + spanId: '', + serviceName: 'checkout', + resourceFilter: 'service.version=1.2.3', + operationName: '', + minDurationMs: '250', + errorOnly: false, + spanScope: 'root' + } as any).expression).toContain('FROM hzb_traces'); + expect(buildTraceAlertRuleDraft({ + traceId: '', + spanId: '', + serviceName: 'checkout', + resourceFilter: '', + operationName: '', + minDurationMs: '250', + maxDurationMs: '', + errorOnly: true, + spanScope: 'root' + } as any).expression).toContain('FROM hzb_traces'); + expect(buildTraceAlertRuleDraft({ + traceId: '', + spanId: '', + serviceName: 'checkout', + resourceFilter: '', + operationName: '', + minDurationMs: '', + maxDurationMs: '500', + errorOnly: true, + spanScope: 'root' + } as any).expression).toContain('FROM hzb_traces'); }); it('uses trace detail and selected span HertzBeat attributes when route entity context is missing', () => { diff --git a/web-next/lib/trace-manage/view-model.ts b/web-next/lib/trace-manage/view-model.ts index 6b1a5cc46e..2d2b5d345c 100644 --- a/web-next/lib/trace-manage/view-model.ts +++ b/web-next/lib/trace-manage/view-model.ts @@ -473,26 +473,168 @@ function sanitizeTraceDurationThreshold(value: string | null | undefined) { return parsed; } +function traceDurationMillisToNanos(value: number) { + return value * 1_000_000; +} + +function traceRedResourceColumn(key: string) { + switch (key.trim()) { + case 'service.name': + case 'service_name': + return 'service_name'; + case 'service.namespace': + case 'service_namespace': + return 'service_namespace'; + case 'deployment.environment.name': + case 'deployment.environment': + case 'deployment_environment': + case 'deployment_environment_name': + case 'environment': + return 'deployment_environment'; + case 'hertzbeat.entity_id': + case 'entity_id': + return 'entity_id'; + default: + return undefined; + } +} + +function parseTraceRedResourceFilterClause(clause: string) { + const normalized = clause.trim(); + if (!normalized) return undefined; + + const quotedMatch = normalized.match(/^([A-Za-z0-9_.:-]+)\s*(=|:)\s*(?:"([^"\\\r\n]*)"|'([^'\\\r\n]*)')$/); + if (quotedMatch) { + const column = traceRedResourceColumn(quotedMatch[1]); + const value = sanitizeTraceSqlLiteral(quotedMatch[3] ?? quotedMatch[4] ?? ''); + return column && value ? `${column} = '${value}'` : null; + } + + const rawMatch = normalized.match(/^([A-Za-z0-9_.:-]+)\s*(=|:)\s*([A-Za-z0-9_.:/ -]+)$/); + if (rawMatch) { + const column = traceRedResourceColumn(rawMatch[1]); + const value = sanitizeTraceSqlLiteral(rawMatch[3]); + return column && value ? `${column} = '${value}'` : null; + } + + return null; +} + +function buildTraceRedResourceFilterClauses(resourceFilter: string | null | undefined) { + const normalized = resourceFilter?.trim(); + if (!normalized) return []; + + const clauses = normalized.split(/\s+and\s+|\s*,\s*/i).map(clause => clause.trim()).filter(Boolean); + if (clauses.length === 0) return []; + + const expressions = clauses.map(parseTraceRedResourceFilterClause); + return expressions.every(Boolean) ? expressions as string[] : undefined; +} + +function parseTraceRawResourceFilterClause(clause: string) { + const normalized = clause.trim(); + if (!normalized) return undefined; + + const quotedMatch = normalized.match(/^([A-Za-z0-9_.:-]+)\s*(=|:)\s*(?:"([^"\\\r\n]*)"|'([^'\\\r\n]*)')$/); + if (quotedMatch) { + const value = sanitizeTraceSqlLiteral(quotedMatch[3] ?? quotedMatch[4] ?? ''); + return value ? traceRawResourceFilterExpression(quotedMatch[1], value) : null; + } + + const rawMatch = normalized.match(/^([A-Za-z0-9_.:-]+)\s*(=|:)\s*([A-Za-z0-9_.:/ -]+)$/); + if (rawMatch) { + const value = sanitizeTraceSqlLiteral(rawMatch[3]); + return value ? traceRawResourceFilterExpression(rawMatch[1], value) : null; + } + + return null; +} + +function traceRawResourceFilterExpression(key: string, value: string) { + const normalizedKey = key.trim(); + if (!/^[A-Za-z0-9_.:-]+$/.test(normalizedKey)) return null; + if (normalizedKey === 'service.name' || normalizedKey === 'service_name') { + return `service_name = '${value}'`; + } + return `json_get_string(resource_attributes, '$["${normalizedKey}"]') = '${value}'`; +} + +function buildTraceRawResourceFilterClauses(resourceFilter: string | null | undefined) { + const normalized = resourceFilter?.trim(); + if (!normalized) return []; + + const clauses = normalized.split(/\s+and\s+|\s*,\s*/i).map(clause => clause.trim()).filter(Boolean); + if (clauses.length === 0) return []; + + const expressions = clauses.map(parseTraceRawResourceFilterClause); + return expressions.every(Boolean) ? expressions as string[] : undefined; +} + function buildTraceRedWhereClauses(query: TraceQueryState, routeContext: SignalRouteContext) { - const serviceName = sanitizeTraceSqlLiteral(query.serviceName || routeContext.serviceName); + const resourceFilterClauses = buildTraceRedResourceFilterClauses(query.resourceFilter); + if (!resourceFilterClauses) return undefined; + const resourceServiceNameClause = resourceFilterClauses.find(clause => clause.startsWith('service_name = ')); + const resourceServiceName = resourceServiceNameClause?.match(/^service_name = '(.+)'$/)?.[1]; + const serviceName = sanitizeTraceSqlLiteral(query.serviceName || routeContext.serviceName) || resourceServiceName; if (!serviceName) return undefined; const operationName = sanitizeOptionalTraceSqlLiteral(query.operationName); const entityId = sanitizeOptionalTraceSqlLiteral(routeContext.entityId); const serviceNamespace = sanitizeOptionalTraceSqlLiteral(routeContext.serviceNamespace); const environment = sanitizeOptionalTraceSqlLiteral(routeContext.environment); if (![operationName, entityId, serviceNamespace, environment].every(filter => filter.valid)) return undefined; - return [ + return Array.from(new Set([ `service_name = '${serviceName}'`, operationName.value ? `operation = '${operationName.value}'` : undefined, entityId.value ? `entity_id = '${entityId.value}'` : undefined, serviceNamespace.value ? `service_namespace = '${serviceNamespace.value}'` : undefined, environment.value ? `deployment_environment = '${environment.value}'` : undefined, + ...resourceFilterClauses, "time_window >= NOW() - INTERVAL '5 minutes'" - ].filter((clause): clause is string => Boolean(clause)); + ].filter((clause): clause is string => Boolean(clause)))); +} + +function buildTraceRawWhereClauses(query: TraceQueryState, routeContext: SignalRouteContext) { + const resourceFilterClauses = buildTraceRawResourceFilterClauses(query.resourceFilter); + if (!resourceFilterClauses) return undefined; + const resourceServiceNameClause = resourceFilterClauses.find(clause => clause.startsWith('service_name = ')); + const resourceServiceName = resourceServiceNameClause?.match(/^service_name = '(.+)'$/)?.[1]; + const serviceName = sanitizeTraceSqlLiteral(query.serviceName || routeContext.serviceName) || resourceServiceName; + if (!serviceName) return undefined; + const operationName = sanitizeOptionalTraceSqlLiteral(query.operationName); + const traceId = sanitizeOptionalTraceSqlLiteral(query.traceId || routeContext.traceId); + const spanId = sanitizeOptionalTraceSqlLiteral(query.spanId || routeContext.spanId); + const entityId = sanitizeOptionalTraceSqlLiteral(routeContext.entityId); + const serviceNamespace = sanitizeOptionalTraceSqlLiteral(routeContext.serviceNamespace); + const environment = sanitizeOptionalTraceSqlLiteral(routeContext.environment); + if (![operationName, traceId, spanId, entityId, serviceNamespace, environment].every(filter => filter.valid)) return undefined; + const minDurationMs = sanitizeTraceDurationThreshold(query.minDurationMs); + const maxDurationMs = sanitizeTraceDurationThreshold(query.maxDurationMs); + if (query.minDurationMs?.trim() && minDurationMs == null) return undefined; + if (query.maxDurationMs?.trim() && maxDurationMs == null) return undefined; + if (minDurationMs != null && maxDurationMs != null && maxDurationMs < minDurationMs) return undefined; + const scope = query.spanScope?.trim().toLowerCase(); + return Array.from(new Set([ + `service_name = '${serviceName}'`, + operationName.value ? `span_name = '${operationName.value}'` : undefined, + traceId.value ? `trace_id = '${traceId.value}'` : undefined, + spanId.value ? `span_id = '${spanId.value}'` : undefined, + entityId.value ? `json_get_string(resource_attributes, '$["hertzbeat.entity_id"]') = '${entityId.value}'` : undefined, + serviceNamespace.value ? `json_get_string(resource_attributes, '$["service.namespace"]') = '${serviceNamespace.value}'` : undefined, + environment.value ? `json_get_string(resource_attributes, '$["deployment.environment.name"]') = '${environment.value}'` : undefined, + minDurationMs != null ? `duration_nano >= ${traceDurationMillisToNanos(minDurationMs)}` : undefined, + maxDurationMs != null ? `duration_nano <= ${traceDurationMillisToNanos(maxDurationMs)}` : undefined, + scope === 'root' ? "(parent_span_id IS NULL OR parent_span_id = '')" : undefined, + scope === 'entrypoint' || scope === 'entrypoint-spans' || scope === 'entry' + ? "(parent_span_id IS NULL OR parent_span_id = '' OR UPPER(span_kind) IN ('SPAN_KIND_SERVER', 'SERVER', 'SPAN_KIND_CONSUMER', 'CONSUMER'))" + : undefined, + ...resourceFilterClauses, + "`timestamp` >= NOW() - INTERVAL '5 minutes'" + ].filter((clause): clause is string => Boolean(clause)))); } function buildTraceErrorRateSql(query: TraceQueryState, routeContext: SignalRouteContext) { if (!query.errorOnly) return undefined; + if (query.minDurationMs?.trim() || query.maxDurationMs?.trim()) return undefined; const whereClauses = buildTraceRedWhereClauses(query, routeContext); if (!whereClauses) return undefined; return [ @@ -505,26 +647,78 @@ function buildTraceErrorRateSql(query: TraceQueryState, routeContext: SignalRout ].join(' '); } +function buildTraceRawErrorSql(query: TraceQueryState, routeContext: SignalRouteContext) { + if (!query.errorOnly) return undefined; + const whereClauses = buildTraceRawWhereClauses(query, routeContext); + if (!whereClauses) return undefined; + return [ + 'SELECT service_name, span_name AS operation, span_kind, COUNT(*) AS __value__', + 'FROM hzb_traces', + `WHERE ${[ + ...whereClauses, + "span_status_code IN ('STATUS_CODE_ERROR', 'ERROR')" + ].join(' AND ')}`, + 'GROUP BY service_name, span_name, span_kind', + 'HAVING __value__ > 0' + ].join(' '); +} + function buildTraceLatencySql(query: TraceQueryState, routeContext: SignalRouteContext) { if (query.errorOnly) return undefined; const minDurationMs = sanitizeTraceDurationThreshold(query.minDurationMs); - if (minDurationMs == null) return undefined; + const maxDurationMs = sanitizeTraceDurationThreshold(query.maxDurationMs); + if (minDurationMs == null && maxDurationMs == null) return undefined; + if (query.minDurationMs?.trim() && minDurationMs == null) return undefined; + if (query.maxDurationMs?.trim() && maxDurationMs == null) return undefined; + if (minDurationMs != null && maxDurationMs != null && maxDurationMs < minDurationMs) return undefined; const whereClauses = buildTraceRedWhereClauses(query, routeContext); if (!whereClauses) return undefined; + const havingClauses = [ + minDurationMs != null ? `__value__ >= ${minDurationMs}` : undefined, + maxDurationMs != null ? `__value__ <= ${maxDurationMs}` : undefined + ].filter((clause): clause is string => Boolean(clause)); return [ 'SELECT service_name, operation, span_kind,', 'SUM(duration_sum_nano) / NULLIF(SUM(duration_count), 0) / 1000000 AS __value__', 'FROM hertzbeat_apm_red_1m', `WHERE ${whereClauses.join(' AND ')}`, 'GROUP BY service_name, operation, span_kind', - `HAVING __value__ >= ${minDurationMs}` + `HAVING ${havingClauses.join(' AND ')}` + ].join(' '); +} + +function buildTraceRawLatencySql(query: TraceQueryState, routeContext: SignalRouteContext) { + if (query.errorOnly) return undefined; + const minDurationMs = sanitizeTraceDurationThreshold(query.minDurationMs); + const maxDurationMs = sanitizeTraceDurationThreshold(query.maxDurationMs); + if (minDurationMs == null && maxDurationMs == null) return undefined; + if (query.minDurationMs?.trim() && minDurationMs == null) return undefined; + if (query.maxDurationMs?.trim() && maxDurationMs == null) return undefined; + if (minDurationMs != null && maxDurationMs != null && maxDurationMs < minDurationMs) return undefined; + const whereClauses = buildTraceRawWhereClauses(query, routeContext); + if (!whereClauses) return undefined; + const havingClauses = [ + minDurationMs != null ? `__value__ >= ${minDurationMs}` : undefined, + maxDurationMs != null ? `__value__ <= ${maxDurationMs}` : undefined + ].filter((clause): clause is string => Boolean(clause)); + return [ + 'SELECT service_name, span_name AS operation, span_kind,', + 'SUM(duration_nano) / NULLIF(COUNT(duration_nano), 0) / 1000000 AS __value__', + 'FROM hzb_traces', + `WHERE ${whereClauses.join(' AND ')}`, + 'GROUP BY service_name, span_name, span_kind', + `HAVING ${havingClauses.join(' AND ')}` ].join(' '); } export function buildTraceAlertRuleDraft(query: TraceQueryState, routeContext: SignalRouteContext = {}): SignalAlertRuleDraftContext { const errorRateExpression = buildTraceErrorRateSql(query, routeContext); const latencyExpression = errorRateExpression ? undefined : buildTraceLatencySql(query, routeContext); - const expression = errorRateExpression || latencyExpression; + const rawErrorExpression = errorRateExpression || latencyExpression ? undefined : buildTraceRawErrorSql(query, routeContext); + const rawLatencyExpression = errorRateExpression || latencyExpression || rawErrorExpression + ? undefined + : buildTraceRawLatencySql(query, routeContext); + const expression = errorRateExpression || latencyExpression || rawErrorExpression || rawLatencyExpression; const parts = [ ['traceId', query.traceId || routeContext.traceId], ['spanId', query.spanId || routeContext.spanId], @@ -550,7 +744,7 @@ export function buildTraceAlertRuleDraft(query: TraceQueryState, routeContext: S expression, ...(expression ? { datasource: 'sql', - template: latencyExpression + template: latencyExpression || rawLatencyExpression ? 'Trace latency detected ${service_name} ${operation}: ${__value__} ms' : 'Trace error rate detected ${service_name} ${operation}: ${__value__}' } : {}) --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
