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]

Reply via email to