Compare commits

...

4 Commits

Author SHA1 Message Date
srikanthccv
fdefce8150 feat(semconv): enable the rollout families and exact field resolution
The overlay enables the database, RPC, messaging, container, code, and
HTTP attribute families. `exact(key)` in a filter expression, or
`fieldResolution: exact` on a structured key, reads one stored spelling.
Product defaults and emitters use the current names; a migration renames
stored quick filters. The generator lints product code for old names.

Assisted-by: Claude Fable 5.1
2026-10-02 03:31:57 +05:30
srikanthccv
04021842f0 feat(semconv): add migration product surfaces
Product constants and defaults take the current semantic-convention
names. Span-metrics label keys keep the collector's spelling until the
metrics rename ships. The related-values table holds no metrics rows, so
the migration report covers traces and logs.

Assisted-by: Claude Fable 5.1
2026-10-02 02:34:45 +05:30
Srikanth Chekuri
1643df620b feat: resolve semconv families across logs and metrics (#12870)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

Phase 2 of #6143: semantic-convention families resolve on logs and
metrics, behind the `resolve_semconv_families` flag (default off), on
the storage contract of #12802.

- Registry: each member carries the scope of its own rename edges, so a
fan-out keeps one membership per target and an ambiguous name stays
literal.
- Logs and metrics families need no family code of their own. The gate
applies to every signal, and `LogicalRead` merges members through each
storage's `Read`.
- Metric-name families union the storage names in every `metric_name`
filter, and the querier reads type, temporality, and the reduced flag
across the family.
- Span-metrics labels: the metrics the processor emits, listed by name,
also read each family member with the `resource_` prefix. Requested
names are never rewritten.
- Values suggestions and related values cover every spelling of the
family.
- `deployment.environment.name` resolves on all three signals.
`db.system.name` stays off until a value-mapping reader exists.

#### Additional Information

- `pkg/semconv.Family` fields are now unexported, and `transition.go` is
removed. #12446 reads the old API and needs an update when stacked.
- A target that emits both names of a metric-name family double-counts
in `sum()` during the overlap window. Reading both names is the feature.
Pinned by a test.
- A family of metrics labels keeps the keyless contract of a single
label: no guard, no NULL group.
2026-10-01 20:28:15 +00:00
Aditya Singh
fd21f8b760 feat(bottom-strip): show the page count on the left for noz, alerts and home (#12963)
#### Description

- noz page, alert rules, home, exceptions and services now push their
count to the left of the strip. each page has its own `useXStripInfo`
hook that builds the config and publishes it.. same pattern trace
details already uses, five more times.
- the hook does the whole thing, so the page call is one line. noz and
home fetch or subscribe inside the hook so the page does not re-render
just to keep the strip current.. the rest pass values they already hold.
- alert rules and exceptions show "N of M".. first number is what is on
the page right now. both read the same two values the table hands its
own pagination, so the strip cannot disagree with the table. services,
home and noz are a plain count.
- services renders one of two tables on `use_span_metrics`, so the hook
is called from both.. the count text lives in one place either way.
- dashboards is left out for now, it already shows this on its own
strip.

#### Issues closed by this PR

Part of https://github.com/SigNoz/events-pod/issues/53
Part of https://github.com/SigNoz/events-pod/issues/55


#### Screenshots

Ai Assistant  Bottom strip

<img width="3456" height="1968" alt="image"
src="https://github.com/user-attachments/assets/0042f71b-1b56-417a-8283-af9ba9596351"
/>


Alert rules

<img width="3452" height="1992" alt="image"
src="https://github.com/user-attachments/assets/a674de13-9841-4717-89ce-a656c9df646c"
/>

Exceptions 

<img width="3456" height="1970" alt="image"
src="https://github.com/user-attachments/assets/6333ea6b-2342-46dc-985d-2855bebe4003"
/>



Services

<img width="3456" height="1996" alt="image"
src="https://github.com/user-attachments/assets/abe0c5cb-cfe3-48b6-9591-ee71e677a1a6"
/>




#### Additional Information
2026-10-01 19:30:00 +00:00
184 changed files with 8337 additions and 1846 deletions

View File

@@ -66,8 +66,8 @@ jobs:
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: check-semconv-generated-files
run: go run ./scripts/semconv -check
- name: check-semconv-generated-files-and-product-literals
run: make semconv-check
build:
if: |
github.event_name == 'merge_group' ||

View File

@@ -262,6 +262,10 @@ py-clean: ## Clear all pycache and pytest cache from tests directory recursively
semconv-generate: ## Regenerate semantic-convention families for Go and TypeScript
@go run ./scripts/semconv
.PHONY: semconv-check
semconv-check: ## Verify generated semantic-convention files and reject old-name product literals
@go run ./scripts/semconv -check -lint
.PHONY: gen-mocks
gen-mocks:
@echo ">> Generating mocks"

View File

@@ -7792,6 +7792,8 @@ components:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
fieldDataType:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
fieldResolution:
$ref: '#/components/schemas/TelemetrytypesFieldResolution'
meta:
properties:
unit:
@@ -7836,6 +7838,10 @@ components:
rowsScanned:
minimum: 0
type: integer
semconvResolutions:
items:
$ref: '#/components/schemas/Querybuildertypesv5SemconvResolution'
type: array
stepIntervals:
additionalProperties:
minimum: 0
@@ -7902,6 +7908,8 @@ components:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
fieldDataType:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
fieldResolution:
$ref: '#/components/schemas/TelemetrytypesFieldResolution'
name:
type: string
signal:
@@ -7989,6 +7997,8 @@ components:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
fieldDataType:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
fieldResolution:
$ref: '#/components/schemas/TelemetrytypesFieldResolution'
name:
type: string
signal:
@@ -8565,6 +8575,20 @@ components:
stepInterval:
$ref: '#/components/schemas/Querybuildertypesv5Step'
type: object
Querybuildertypesv5SemconvResolution:
properties:
current:
type: string
kind:
type: string
members:
items:
type: string
nullable: true
type: array
requested:
type: string
type: object
Querybuildertypesv5Step:
description: Step interval. Accepts a Go duration string (e.g., "60s", "1m",
"1h") or a number representing seconds (e.g., 60).
@@ -10347,6 +10371,11 @@ components:
- number
- ""
type: string
TelemetrytypesFieldResolution:
enum:
- exact
- ""
type: string
TelemetrytypesGettableFieldKeys:
properties:
complete:
@@ -10372,6 +10401,42 @@ components:
- values
- complete
type: object
TelemetrytypesGettableSemconvMigrationReport:
properties:
endUnixMilli:
format: int64
type: integer
entries:
items:
$ref: '#/components/schemas/TelemetrytypesSemconvMigrationReportEntry'
nullable: true
type: array
startUnixMilli:
format: int64
type: integer
required:
- entries
type: object
TelemetrytypesSemconvMigrationReportEntry:
properties:
current:
type: string
lastSeenUnixMilli:
format: int64
type: integer
old:
type: string
resourceSets:
minimum: 0
type: integer
services:
items:
type: string
nullable: true
type: array
signal:
type: string
type: object
TelemetrytypesSignal:
enum:
- traces
@@ -10392,6 +10457,8 @@ components:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
fieldDataType:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
fieldResolution:
$ref: '#/components/schemas/TelemetrytypesFieldResolution'
name:
type: string
signal:
@@ -12739,6 +12806,65 @@ paths:
tags:
- fields
x-signoz-stability: alpha
/api/v1/fields/semconv-migration:
get:
deprecated: false
description: Returns services that still emit old semantic-convention names
without the current family name
operationId: GetSemconvMigrationReport
parameters:
- in: query
name: startUnixMilli
schema:
format: int64
type: integer
- in: query
name: endUnixMilli
schema:
format: int64
type: integer
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TelemetrytypesGettableSemconvMigrationReport'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get semantic-convention migration report
tags:
- fields
x-signoz-stability: alpha
/api/v1/fields/values:
get:
deprecated: false
@@ -21261,8 +21387,8 @@ paths:
alert: Payments-api error log rate above 1%
alertType: LOGS_BASED_ALERT
annotations:
description: Error log rate in {{$deployment.environment}} is
{{$value}}%
description: Error log rate in {{$deployment.environment.name}}
is {{$value}}%
summary: Payments-api error rate above {{$threshold}}%
condition:
compositeQuery:
@@ -21278,7 +21404,7 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: A
signal: logs
stepInterval: 60
@@ -21292,14 +21418,14 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: B
signal: logs
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{deployment.environment}}'
legend: '{{deployment.environment.name}}'
name: F1
type: builder_formula
queryType: builder
@@ -21325,7 +21451,7 @@ paths:
team: payments
notificationSettings:
groupBy:
- deployment.environment
- deployment.environment.name
renotify:
alertStates:
- firing
@@ -21344,7 +21470,7 @@ paths:
alertType: LOGS_BASED_ALERT
annotations:
description: '{{$k8s.pod.name}} emitted {{$value}} panic log(s)
in {{$deployment.environment}}.'
in {{$deployment.environment.name}}.'
summary: Payments service panic
condition:
compositeQuery:
@@ -21362,8 +21488,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: A
signal: logs
stepInterval: 60
@@ -21392,7 +21518,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment
- deployment.environment.name
renotify:
alertStates:
- firing
@@ -21462,8 +21588,9 @@ paths:
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
resource attributes are quoted ("deployment.environment"). Useful
for queries that combine series with group_right or other Prom operators.
resource attributes are quoted ("deployment.environment.name").
Useful for queries that combine series with group_right or other
Prom operators.
summary: Metric threshold PromQL rule
value:
alert: Kafka consumer group lag above 1000
@@ -21479,9 +21606,9 @@ paths:
- spec:
legend: '{{topic}}/{{partition}} ({{group}})'
name: A
query: (max by(topic, partition, "deployment.environment")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment") group_right
max by(group, topic, partition, "deployment.environment")(kafka_consumer_committed_offset))
query: (max by(topic, partition, "deployment.environment.name")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment.name")
group_right max by(group, topic, partition, "deployment.environment.name")(kafka_consumer_committed_offset))
> 0
type: promql
queryType: promql
@@ -21617,7 +21744,7 @@ paths:
alertType: METRIC_BASED_ALERT
annotations:
description: Pod {{$k8s.pod.name}} CPU is at {{$value}} of request
in {{$deployment.environment}}.
in {{$deployment.environment.name}}.
summary: Pod CPU above {{$threshold}} of request
condition:
compositeQuery:
@@ -21636,8 +21763,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: A
signal: metrics
stepInterval: 60
@@ -21668,7 +21795,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment
- deployment.environment.name
renotify:
alertStates:
- firing
@@ -21692,7 +21819,7 @@ paths:
alert: API 5xx error rate above 1%
alertType: TRACES_BASED_ALERT
annotations:
description: '{{$service.name}} 5xx rate in {{$deployment.environment}}
description: '{{$service.name}} 5xx rate in {{$deployment.environment.name}}
is {{$value}}%.'
summary: API service error rate elevated
condition:
@@ -21704,7 +21831,7 @@ paths:
- expression: count()
disabled: true
filter:
expression: service.name CONTAINS 'api' AND http.status_code
expression: service.name CONTAINS 'api' AND http.response.status_code
>= 500
groupBy:
- fieldContext: resource
@@ -21712,7 +21839,7 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: A
signal: traces
stepInterval: 60
@@ -21729,14 +21856,14 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: B
signal: traces
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{service.name}} ({{deployment.environment}})'
legend: '{{service.name}} ({{deployment.environment.name}})'
name: F1
type: builder_formula
queryType: builder
@@ -21764,7 +21891,7 @@ paths:
notificationSettings:
groupBy:
- service.name
- deployment.environment
- deployment.environment.name
newGroupEvalDelay: 2m
renotify:
alertStates:
@@ -22213,8 +22340,8 @@ paths:
alert: Payments-api error log rate above 1%
alertType: LOGS_BASED_ALERT
annotations:
description: Error log rate in {{$deployment.environment}} is
{{$value}}%
description: Error log rate in {{$deployment.environment.name}}
is {{$value}}%
summary: Payments-api error rate above {{$threshold}}%
condition:
compositeQuery:
@@ -22230,7 +22357,7 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: A
signal: logs
stepInterval: 60
@@ -22244,14 +22371,14 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: B
signal: logs
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{deployment.environment}}'
legend: '{{deployment.environment.name}}'
name: F1
type: builder_formula
queryType: builder
@@ -22277,7 +22404,7 @@ paths:
team: payments
notificationSettings:
groupBy:
- deployment.environment
- deployment.environment.name
renotify:
alertStates:
- firing
@@ -22296,7 +22423,7 @@ paths:
alertType: LOGS_BASED_ALERT
annotations:
description: '{{$k8s.pod.name}} emitted {{$value}} panic log(s)
in {{$deployment.environment}}.'
in {{$deployment.environment.name}}.'
summary: Payments service panic
condition:
compositeQuery:
@@ -22314,8 +22441,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: A
signal: logs
stepInterval: 60
@@ -22344,7 +22471,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment
- deployment.environment.name
renotify:
alertStates:
- firing
@@ -22414,8 +22541,9 @@ paths:
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
resource attributes are quoted ("deployment.environment"). Useful
for queries that combine series with group_right or other Prom operators.
resource attributes are quoted ("deployment.environment.name").
Useful for queries that combine series with group_right or other
Prom operators.
summary: Metric threshold PromQL rule
value:
alert: Kafka consumer group lag above 1000
@@ -22431,9 +22559,9 @@ paths:
- spec:
legend: '{{topic}}/{{partition}} ({{group}})'
name: A
query: (max by(topic, partition, "deployment.environment")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment") group_right
max by(group, topic, partition, "deployment.environment")(kafka_consumer_committed_offset))
query: (max by(topic, partition, "deployment.environment.name")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment.name")
group_right max by(group, topic, partition, "deployment.environment.name")(kafka_consumer_committed_offset))
> 0
type: promql
queryType: promql
@@ -22569,7 +22697,7 @@ paths:
alertType: METRIC_BASED_ALERT
annotations:
description: Pod {{$k8s.pod.name}} CPU is at {{$value}} of request
in {{$deployment.environment}}.
in {{$deployment.environment.name}}.
summary: Pod CPU above {{$threshold}} of request
condition:
compositeQuery:
@@ -22588,8 +22716,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: A
signal: metrics
stepInterval: 60
@@ -22620,7 +22748,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment
- deployment.environment.name
renotify:
alertStates:
- firing
@@ -22644,7 +22772,7 @@ paths:
alert: API 5xx error rate above 1%
alertType: TRACES_BASED_ALERT
annotations:
description: '{{$service.name}} 5xx rate in {{$deployment.environment}}
description: '{{$service.name}} 5xx rate in {{$deployment.environment.name}}
is {{$value}}%.'
summary: API service error rate elevated
condition:
@@ -22656,7 +22784,7 @@ paths:
- expression: count()
disabled: true
filter:
expression: service.name CONTAINS 'api' AND http.status_code
expression: service.name CONTAINS 'api' AND http.response.status_code
>= 500
groupBy:
- fieldContext: resource
@@ -22664,7 +22792,7 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: A
signal: traces
stepInterval: 60
@@ -22681,14 +22809,14 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: B
signal: traces
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{service.name}} ({{deployment.environment}})'
legend: '{{service.name}} ({{deployment.environment.name}})'
name: F1
type: builder_formula
queryType: builder
@@ -22716,7 +22844,7 @@ paths:
notificationSettings:
groupBy:
- service.name
- deployment.environment
- deployment.environment.name
newGroupEvalDelay: 2m
renotify:
alertStates:
@@ -23066,8 +23194,8 @@ paths:
alert: Payments-api error log rate above 1%
alertType: LOGS_BASED_ALERT
annotations:
description: Error log rate in {{$deployment.environment}} is
{{$value}}%
description: Error log rate in {{$deployment.environment.name}}
is {{$value}}%
summary: Payments-api error rate above {{$threshold}}%
condition:
compositeQuery:
@@ -23083,7 +23211,7 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: A
signal: logs
stepInterval: 60
@@ -23097,14 +23225,14 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: B
signal: logs
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{deployment.environment}}'
legend: '{{deployment.environment.name}}'
name: F1
type: builder_formula
queryType: builder
@@ -23130,7 +23258,7 @@ paths:
team: payments
notificationSettings:
groupBy:
- deployment.environment
- deployment.environment.name
renotify:
alertStates:
- firing
@@ -23149,7 +23277,7 @@ paths:
alertType: LOGS_BASED_ALERT
annotations:
description: '{{$k8s.pod.name}} emitted {{$value}} panic log(s)
in {{$deployment.environment}}.'
in {{$deployment.environment.name}}.'
summary: Payments service panic
condition:
compositeQuery:
@@ -23167,8 +23295,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: A
signal: logs
stepInterval: 60
@@ -23197,7 +23325,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment
- deployment.environment.name
renotify:
alertStates:
- firing
@@ -23267,8 +23395,9 @@ paths:
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
resource attributes are quoted ("deployment.environment"). Useful
for queries that combine series with group_right or other Prom operators.
resource attributes are quoted ("deployment.environment.name").
Useful for queries that combine series with group_right or other
Prom operators.
summary: Metric threshold PromQL rule
value:
alert: Kafka consumer group lag above 1000
@@ -23284,9 +23413,9 @@ paths:
- spec:
legend: '{{topic}}/{{partition}} ({{group}})'
name: A
query: (max by(topic, partition, "deployment.environment")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment") group_right
max by(group, topic, partition, "deployment.environment")(kafka_consumer_committed_offset))
query: (max by(topic, partition, "deployment.environment.name")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment.name")
group_right max by(group, topic, partition, "deployment.environment.name")(kafka_consumer_committed_offset))
> 0
type: promql
queryType: promql
@@ -23422,7 +23551,7 @@ paths:
alertType: METRIC_BASED_ALERT
annotations:
description: Pod {{$k8s.pod.name}} CPU is at {{$value}} of request
in {{$deployment.environment}}.
in {{$deployment.environment.name}}.
summary: Pod CPU above {{$threshold}} of request
condition:
compositeQuery:
@@ -23441,8 +23570,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: A
signal: metrics
stepInterval: 60
@@ -23473,7 +23602,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment
- deployment.environment.name
renotify:
alertStates:
- firing
@@ -23497,7 +23626,7 @@ paths:
alert: API 5xx error rate above 1%
alertType: TRACES_BASED_ALERT
annotations:
description: '{{$service.name}} 5xx rate in {{$deployment.environment}}
description: '{{$service.name}} 5xx rate in {{$deployment.environment.name}}
is {{$value}}%.'
summary: API service error rate elevated
condition:
@@ -23509,7 +23638,7 @@ paths:
- expression: count()
disabled: true
filter:
expression: service.name CONTAINS 'api' AND http.status_code
expression: service.name CONTAINS 'api' AND http.response.status_code
>= 500
groupBy:
- fieldContext: resource
@@ -23517,7 +23646,7 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: A
signal: traces
stepInterval: 60
@@ -23534,14 +23663,14 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: B
signal: traces
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{service.name}} ({{deployment.environment}})'
legend: '{{service.name}} ({{deployment.environment.name}})'
name: F1
type: builder_formula
queryType: builder
@@ -23569,7 +23698,7 @@ paths:
notificationSettings:
groupBy:
- service.name
- deployment.environment
- deployment.environment.name
newGroupEvalDelay: 2m
renotify:
alertStates:
@@ -24428,8 +24557,8 @@ paths:
alert: Payments-api error log rate above 1%
alertType: LOGS_BASED_ALERT
annotations:
description: Error log rate in {{$deployment.environment}} is
{{$value}}%
description: Error log rate in {{$deployment.environment.name}}
is {{$value}}%
summary: Payments-api error rate above {{$threshold}}%
condition:
compositeQuery:
@@ -24445,7 +24574,7 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: A
signal: logs
stepInterval: 60
@@ -24459,14 +24588,14 @@ paths:
groupBy:
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: B
signal: logs
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{deployment.environment}}'
legend: '{{deployment.environment.name}}'
name: F1
type: builder_formula
queryType: builder
@@ -24492,7 +24621,7 @@ paths:
team: payments
notificationSettings:
groupBy:
- deployment.environment
- deployment.environment.name
renotify:
alertStates:
- firing
@@ -24511,7 +24640,7 @@ paths:
alertType: LOGS_BASED_ALERT
annotations:
description: '{{$k8s.pod.name}} emitted {{$value}} panic log(s)
in {{$deployment.environment}}.'
in {{$deployment.environment.name}}.'
summary: Payments service panic
condition:
compositeQuery:
@@ -24529,8 +24658,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: A
signal: logs
stepInterval: 60
@@ -24559,7 +24688,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment
- deployment.environment.name
renotify:
alertStates:
- firing
@@ -24629,8 +24758,9 @@ paths:
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
resource attributes are quoted ("deployment.environment"). Useful
for queries that combine series with group_right or other Prom operators.
resource attributes are quoted ("deployment.environment.name").
Useful for queries that combine series with group_right or other
Prom operators.
summary: Metric threshold PromQL rule
value:
alert: Kafka consumer group lag above 1000
@@ -24646,9 +24776,9 @@ paths:
- spec:
legend: '{{topic}}/{{partition}} ({{group}})'
name: A
query: (max by(topic, partition, "deployment.environment")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment") group_right
max by(group, topic, partition, "deployment.environment")(kafka_consumer_committed_offset))
query: (max by(topic, partition, "deployment.environment.name")(kafka_log_end_offset)
- on(topic, partition, "deployment.environment.name")
group_right max by(group, topic, partition, "deployment.environment.name")(kafka_consumer_committed_offset))
> 0
type: promql
queryType: promql
@@ -24784,7 +24914,7 @@ paths:
alertType: METRIC_BASED_ALERT
annotations:
description: Pod {{$k8s.pod.name}} CPU is at {{$value}} of request
in {{$deployment.environment}}.
in {{$deployment.environment.name}}.
summary: Pod CPU above {{$threshold}} of request
condition:
compositeQuery:
@@ -24803,8 +24933,8 @@ paths:
name: k8s.pod.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
legend: '{{k8s.pod.name}} ({{deployment.environment}})'
name: deployment.environment.name
legend: '{{k8s.pod.name}} ({{deployment.environment.name}})'
name: A
signal: metrics
stepInterval: 60
@@ -24835,7 +24965,7 @@ paths:
notificationSettings:
groupBy:
- k8s.pod.name
- deployment.environment
- deployment.environment.name
renotify:
alertStates:
- firing
@@ -24859,7 +24989,7 @@ paths:
alert: API 5xx error rate above 1%
alertType: TRACES_BASED_ALERT
annotations:
description: '{{$service.name}} 5xx rate in {{$deployment.environment}}
description: '{{$service.name}} 5xx rate in {{$deployment.environment.name}}
is {{$value}}%.'
summary: API service error rate elevated
condition:
@@ -24871,7 +25001,7 @@ paths:
- expression: count()
disabled: true
filter:
expression: service.name CONTAINS 'api' AND http.status_code
expression: service.name CONTAINS 'api' AND http.response.status_code
>= 500
groupBy:
- fieldContext: resource
@@ -24879,7 +25009,7 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: A
signal: traces
stepInterval: 60
@@ -24896,14 +25026,14 @@ paths:
name: service.name
- fieldContext: resource
fieldDataType: string
name: deployment.environment
name: deployment.environment.name
name: B
signal: traces
stepInterval: 60
type: builder_query
- spec:
expression: (A / B) * 100
legend: '{{service.name}} ({{deployment.environment}})'
legend: '{{service.name}} ({{deployment.environment.name}})'
name: F1
type: builder_formula
queryType: builder
@@ -24931,7 +25061,7 @@ paths:
notificationSettings:
groupBy:
- service.name
- deployment.environment
- deployment.environment.name
newGroupEvalDelay: 2m
renotify:
alertStates:

View File

@@ -84,9 +84,9 @@ A storage answers four questions and nothing else:
| WhenAbsent | Absent row reads | Positive filter | Raw select | Multi-candidate column | Field keys |
|---|---|---|---|---|---|
| `AlwaysPresent` | a real value | no guard | no guard | no branch, ends the candidate list | table columns |
| `AbsentIsSentinel` | `''`, 0, false, and that is not a value | exists guard | exists guard | presence branch | map attributes, cast JSON paths, string families |
| `AbsentIsSentinel` | `''`, 0, false, and that is not a value | exists guard | exists guard | presence branch | map attributes, cast JSON paths, string families of such members |
| `AbsentIsNull` | NULL | no guard | no guard | presence branch | multi-era folds, body JSON paths, numeric families |
| `AbsentIsValue` | `''`, and that is the keyless contract | no guard | no guard | no presence branch | metrics labels, rule state history labels |
| `AbsentIsValue` | `''`, and that is the keyless contract | no guard | no guard | no presence branch | metrics labels, rule state history labels, and families of such members |
### The generic layer
@@ -109,7 +109,7 @@ The functions, from the outside in:
| `RejectsBodyFunction(traits, operator)` | Runs before resolution. A storage without body functions (`has`, `hasAny`, `hasAll`, `hasToken`, `search`) errors. The fingerprint side of a split skips the term, because the main query evaluates it. After resolution, `Condition` errors when `has`, `hasAny`, `hasAll`, or `hasToken` lands on a map-backed key (resource, attribute, scope), before the split can drop it. |
| `SharedCondition(...)` | The `Compile` of every storage without its own condition language: `LogicalRead`, the shared data-type collision cast, `OperatorCondition`, then the guard rule. |
| `OperatorCondition(...)` | The operator switch over an already cast read. A storage with its own cast policy composes with it. |
| `LogicalRead(...)` | The only place family expressions are built. A single-member field reads through its member. A family merges the member reads, current member first: `COALESCE(NULLIF(m1, ''), NULLIF(m2, ''), '')` for strings, `multiIf` with a NULL tail for numbers. It ORs the member presence tests. A row without any member reads what the tail of the merge reads. A member with a value map reads through `TransformRead`. `NOT EXISTS` is the read's `Absence`, the storage's own negated form. |
| `LogicalRead(...)` | The only place family expressions are built. A single-member field reads through its member. A family merges the member reads, current member first: `COALESCE(NULLIF(m1, ''), NULLIF(m2, ''), '')` for strings, `multiIf` with a NULL tail for numbers. It ORs the member presence tests. A row without any member reads what the tail of the merge reads. When every member reads its sentinel as a value, so does the family. A member with a value map reads through `TransformRead`. `NOT EXISTS` is the read's `Absence`, the storage's own negated form. |
### A resolved key
@@ -131,7 +131,7 @@ A `LogicalField` is one meaning: one name, context, and data type, backed by one
The resolution order is the same for every storage, in the filter and in every select field, group by, order by, and aggregation:
1. **Own context.** A key under one of the storage's own contexts (`span.x`, `log.x`) matches its own context first, then a column the storage knows under that context. Only when both miss does it look up as if it had no context. `span.http.method` then corrects to the attribute. Strict contexts (`resource.`, `attribute.`, `scope.`, `body.`) are kept as written.
2. **Matches.** The metadata keys under the key's spellings, grouped into families when the flag is on. Each combination of context and data type is one interpretation.
2. **Matches.** The metadata keys under the key's spellings, grouped into families when the flag is on. Each combination of context and data type is one interpretation. A key written as `exact(key)` in an expression, or a structured key with `fieldResolution: exact`, reads its own spelling alone.
3. **Ambiguity.** A filter settles several interpretations by resource over attribute, with a warning. A select field, group by, order by, or aggregation keeps every interpretation in metadata order and folds them, so it shows the value wherever it is.
4. **Intrinsic column first**, bare keys only. A column every row has leads the list. Metadata can report the column, or the storage's `Fallback` can. When only `Fallback` knows the column, its key carries the data type the column reads as (`querybuilder.ColumnDataType`). A same-named metadata key of a contradicting type then drops, and a time column merges none. When metadata reports the column too, every match reads. A metadata gap degrades to the correct column, never to a corrupt metadata key.
5. **Fallback.** With no match, the storage's fallback keys for the key. When the storage ignores unknown keys (a side query whose main query owns the error), the key is `Skipped`. Otherwise a key nothing can serve is an error with suggestions. The not-found warning fires only when every fallback key is a guess, that is, none of them is always present.

View File

@@ -0,0 +1,9 @@
import axios from 'api';
import { SemconvMigrationReport } from 'types/api/semconvMigration';
async function getSemconvMigrationReport(): Promise<SemconvMigrationReport> {
const response = await axios.get('/fields/semconv-migration');
return response.data.data;
}
export default getSemconvMigrationReport;

View File

@@ -64,12 +64,12 @@ export const COMMON_FILTERS = {
SERVER_SPANS: "kind_string = 'Server'",
CLIENT_SPANS: "kind_string = 'Client'",
INTERNAL_SPANS: "kind_string = 'Internal'",
ERROR_SPANS: 'http.status_code >= 400',
SUCCESS_SPANS: 'http.status_code < 400',
ERROR_SPANS: 'http.response.status_code >= 400',
SUCCESS_SPANS: 'http.response.status_code < 400',
// Common service filters
EXCLUDE_HEALTH_CHECKS: "http.route != '/health' AND http.route != '/ping'",
HTTP_REQUESTS: "http.method != ''",
HTTP_REQUESTS: "http.request.method != ''",
// Log filters
ERROR_LOGS: "severity_text = 'ERROR'",
@@ -87,7 +87,7 @@ export const COMMON_GROUP_BY_FIELDS = {
fieldContext: 'resource' as const,
},
HTTP_METHOD: {
name: 'http.method',
name: 'http.request.method',
fieldDataType: 'string' as const,
fieldContext: 'attribute' as const,
},
@@ -97,7 +97,7 @@ export const COMMON_GROUP_BY_FIELDS = {
fieldContext: 'attribute' as const,
},
HTTP_STATUS_CODE: {
name: 'http.status_code',
name: 'http.response.status_code',
fieldDataType: 'int64' as const,
fieldContext: 'attribute' as const,
},

View File

@@ -145,7 +145,7 @@ function CeleryOverviewConfigOptions(): JSX.Element {
{
placeholder: 'Destination',
queryParam: QueryParams.destination,
filterType: ['messaging.destination.name', 'messaging.destination'],
filterType: ['messaging.destination.name'],
},
{
placeholder: 'Kind',

View File

@@ -3,6 +3,7 @@ import { Badge, BadgeColor } from '@signozhq/ui/badge';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
import { ILog } from 'types/api/logs/log';
import { getSemconvMembers } from 'utils/semconv';
import styles from './LogHighlights.module.scss';
import TraceIdField from './TraceIdField';
@@ -79,7 +80,9 @@ export const LOG_HIGHLIGHTS: LogHighlightConfig[] = [
key: 'environment',
label: 'ENVIRONMENT',
render: (log): ReactNode | null => {
const value = getAttr(log, 'deployment.environment');
const value = getSemconvMembers('deployment.environment.name')
.map((key) => getAttr(log, key))
.find(Boolean);
return value ? valueBadge(value) : null;
},
},

View File

@@ -1758,7 +1758,7 @@ QuerySearch.defaultProps = {
signalSource: '',
hardcodedAttributeKeys: undefined,
placeholder:
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')",
"Enter your filter query (e.g., http.response.status_code >= 500 AND service.name = 'frontend')",
showFilterSuggestionsWithoutMetric: false,
initialExpression: undefined,
};

View File

@@ -995,6 +995,16 @@ describe('removeKeysFromExpression', () => {
expect(result).toBe("status = 'success'");
});
it('should remove a comparison that uses the exact field wrapper', () => {
const expression =
"exact(resource.deployment.environment) EXISTS AND service.name = 'api-gateway'";
const result = removeKeysFromExpression(expression, [
'resource.deployment.environment',
]);
expect(result).toBe("service.name = 'api-gateway'");
});
it('should remove multiple keys from expression', () => {
const expression =
"service.name = 'api-gateway' AND status = 'success' AND region = 'us-east-1'";

View File

@@ -687,7 +687,16 @@ export const removeKeysFromExpression = (
}
function visitComparison(ctx: ComparisonContext): string | null {
const keyText = ctx.key().getText().trim().toLowerCase();
const field = ctx.field();
// The runtime returns null for the inactive field alternative even though
// the generated TypeScript signature is non-nullable.
const exactCall = field.exactCall() as unknown as ReturnType<
typeof field.exactCall
> | null;
const keyText = (exactCall ? exactCall.key() : field.key())
.getText()
.trim()
.toLowerCase();
if (!keysSet.has(keyText)) {
return src(ctx);

View File

@@ -5,7 +5,7 @@ import { ArrowUpRight } from '@signozhq/icons';
const QUICK_FILTER_DOC_PATHS: Record<string, string> = {
severity_text: 'severity-text',
'deployment.environment': 'environment',
'deployment.environment.name': 'environment',
'service.name': 'service-name',
'host.name': 'hostname',
'k8s.cluster.name': 'k8s-cluster-name',

View File

@@ -0,0 +1,32 @@
import { Alert } from 'antd';
import { findOldSemconvNames } from 'utils/semconv';
interface SemconvEditorWarningProps {
value: unknown;
editor: string;
}
function SemconvEditorWarning({
value,
editor,
}: SemconvEditorWarningProps): JSX.Element | null {
const text = typeof value === 'string' ? value : JSON.stringify(value ?? '');
const renames = findOldSemconvNames(text);
if (renames.length === 0) {
return null;
}
return (
<Alert
type="warning"
showIcon
data-testid="semconv-editor-warning"
message={`${editor} contains renamed OpenTelemetry fields`}
description={renames
.map(({ old, current }) => `${old} → ${current}`)
.join(', ')}
/>
);
}
export default SemconvEditorWarning;

View File

@@ -0,0 +1,23 @@
import { Badge } from '@signozhq/ui/badge';
import { getSemconvRename } from 'utils/semconv';
interface SemconvOldNameBadgeProps {
name: string;
}
function SemconvOldNameBadge({
name,
}: SemconvOldNameBadgeProps): JSX.Element | null {
const rename = getSemconvRename(name);
if (!rename || rename.family.kind !== 'attribute') {
return null;
}
return (
<Badge color="amber" variant="outline" data-testid="semconv-old-name-badge">
old name, renamed to {rename.current}
</Badge>
);
}
export default SemconvOldNameBadge;

View File

@@ -0,0 +1,39 @@
import { render, screen } from '@testing-library/react';
import SemconvEditorWarning from '../SemconvEditorWarning';
import SemconvOldNameBadge from '../SemconvOldNameBadge';
describe('semantic convention product hints', () => {
it('badges an old raw attribute with its current name', () => {
render(<SemconvOldNameBadge name="deployment.environment" />);
expect(screen.getByTestId('semconv-old-name-badge')).toHaveTextContent(
'old name, renamed to deployment.environment.name',
);
});
it('does not badge a current raw attribute', () => {
render(<SemconvOldNameBadge name="deployment.environment.name" />);
expect(
screen.queryByTestId('semconv-old-name-badge'),
).not.toBeInTheDocument();
});
it('shows an informational editor warning without disabling the editor', () => {
render(
<>
<input aria-label="query" defaultValue="deployment.environment = 'prod'" />
<SemconvEditorWarning
value="deployment.environment = 'prod'"
editor="ClickHouse SQL"
/>
</>,
);
expect(screen.getByLabelText('query')).not.toBeDisabled();
expect(screen.getByTestId('semconv-editor-warning')).toHaveTextContent(
'deployment.environment → deployment.environment.name',
);
});
});

View File

@@ -0,0 +1,2 @@
export { default as SemconvEditorWarning } from './SemconvEditorWarning';
export { default as SemconvOldNameBadge } from './SemconvOldNameBadge';

View File

@@ -1,32 +1,406 @@
// Code generated by scripts/semconv. DO NOT EDIT.
export type SemconvFamily = {
readonly current: string;
readonly old: readonly string[];
readonly kind: 'attribute' | 'metric';
// An empty contexts/signals/applyToMetrics array places no constraint on
// that axis.
export type SemconvMember = {
readonly name: string;
readonly contexts: readonly string[];
readonly signals: readonly string[];
readonly applyToMetrics: readonly string[];
};
export type SemconvFamily = {
readonly current: string;
readonly kind: 'attribute' | 'metric';
readonly members: readonly SemconvMember[];
readonly contexts: readonly string[];
readonly signals: readonly string[];
readonly valueMap: Readonly<Record<string, string>>;
};
export const SEMCONV_FAMILIES: readonly SemconvFamily[] = [
{
current: 'db.system.name',
old: ['db.system'],
current: 'code.file.path',
kind: 'attribute',
members: [
{
name: 'code.filepath',
contexts: [],
signals: [],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'code.function.name',
kind: 'attribute',
members: [
{
name: 'code.function',
contexts: [],
signals: [],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'code.line.number',
kind: 'attribute',
members: [
{
name: 'code.lineno',
contexts: [],
signals: [],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'container.cpu.usage',
kind: 'metric',
members: [
{
name: 'container.cpu.utilization',
contexts: [],
signals: [],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'container.runtime.name',
kind: 'attribute',
members: [
{
name: 'container.runtime',
contexts: [],
signals: [],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'db.namespace',
kind: 'attribute',
members: [
{
name: 'db.elasticsearch.cluster.name',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
},
{
name: 'db.name',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
},
{
name: 'db.cassandra.keyspace',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
},
{
name: 'db.hbase.namespace',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'db.operation.name',
kind: 'attribute',
members: [
{
name: 'db.operation',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'db.query.text',
kind: 'attribute',
members: [
{
name: 'db.statement',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
applyToMetrics: [],
valueMap: {},
},
{
current: 'deployment.environment.name',
old: ['deployment.environment'],
kind: 'attribute',
members: [
{
name: 'deployment.environment',
contexts: [],
signals: [],
applyToMetrics: [],
},
],
contexts: ['attribute', 'resource'],
signals: ['logs', 'metrics', 'traces'],
valueMap: {},
},
{
current: 'http.request.method',
kind: 'attribute',
members: [
{
name: 'http.method',
contexts: ['attribute'],
signals: ['logs', 'traces'],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
applyToMetrics: [],
valueMap: {},
},
{
current: 'http.response.status_code',
kind: 'attribute',
members: [
{
name: 'http.status_code',
contexts: ['attribute'],
signals: ['logs', 'traces'],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'k8s.node.cpu.usage',
kind: 'metric',
members: [
{
name: 'k8s.node.cpu.utilization',
contexts: [],
signals: [],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'k8s.pod.cpu.usage',
kind: 'metric',
members: [
{
name: 'k8s.pod.cpu.utilization',
contexts: [],
signals: [],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'messaging.client.id',
kind: 'attribute',
members: [
{
name: 'messaging.client_id',
contexts: ['attribute'],
signals: ['metrics', 'traces'],
applyToMetrics: [],
},
{
name: 'messaging.kafka.client_id',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
},
{
name: 'messaging.rocketmq.client_id',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'messaging.consumer.group.name',
kind: 'attribute',
members: [
{
name: 'messaging.eventhubs.consumer.group',
contexts: [],
signals: [],
applyToMetrics: [],
},
{
name: 'messaging.kafka.consumer.group',
contexts: [],
signals: [],
applyToMetrics: [],
},
{
name: 'messaging.rocketmq.client_group',
contexts: [],
signals: [],
applyToMetrics: [],
},
{
name: 'messaging.kafka.consumer_group',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'messaging.destination.name',
kind: 'attribute',
members: [
{
name: 'messaging.destination',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'messaging.operation.type',
kind: 'attribute',
members: [
{
name: 'messaging.operation',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'rpc.system.name',
kind: 'attribute',
members: [
{
name: 'rpc.system',
contexts: [],
signals: [],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'service.peer.name',
kind: 'attribute',
members: [
{
name: 'peer.service',
contexts: [],
signals: [],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'url.full',
kind: 'attribute',
members: [
{
name: 'http.url',
contexts: ['attribute'],
signals: ['logs', 'traces'],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'url.scheme',
kind: 'attribute',
members: [
{
name: 'http.scheme',
contexts: ['attribute'],
signals: ['logs', 'traces'],
applyToMetrics: [],
},
],
contexts: [],
signals: [],
valueMap: {},
},
{
current: 'user_agent.original',
kind: 'attribute',
members: [
{
name: 'browser.user_agent',
contexts: ['resource'],
signals: [],
applyToMetrics: [],
},
{
name: 'http.user_agent',
contexts: ['attribute'],
signals: ['traces'],
applyToMetrics: [],
},
],
contexts: ['attribute'],
signals: ['logs', 'traces'],
valueMap: {},
},
] as const;

View File

@@ -10,7 +10,7 @@ export const AllTraceFilterKeyValue: Record<string, string> = {
durationNano: 'Duration',
duration_nano: 'Duration',
durationNanoMax: 'Duration',
'deployment.environment': 'Environment',
'deployment.environment.name': 'Environment',
hasError: 'Status',
has_error: 'Status',
serviceName: 'Service Name',
@@ -55,11 +55,11 @@ export const traceFilterKeys: Record<AllTraceFilterKeys, BaseAutocompleteData> =
id: 'serviceName--string--tag--true',
},
'deployment.environment': {
key: 'deployment.environment',
'deployment.environment.name': {
key: 'deployment.environment.name',
dataType: DataTypes.String,
type: 'resource',
id: 'deployment.environment--string--resource--false',
id: 'deployment.environment.name--string--resource--false',
},
name: {
key: 'name',

View File

@@ -0,0 +1,53 @@
import { renderHook } from '@testing-library/react';
import { useBottomStripStore } from 'container/BottomStrip/store/useBottomStripStore';
import { StripItemKind } from 'container/BottomStrip/types';
import { useExceptionsStripInfo } from '../useExceptionsStripInfo';
describe('useExceptionsStripInfo', () => {
beforeEach(() => {
useBottomStripStore.setState({ left: null, ownerId: null });
});
it('shows the rows on the page against the total', () => {
renderHook(() => useExceptionsStripInfo({ shownCount: 25, totalCount: 500 }));
expect(useBottomStripStore.getState().left).toMatchObject([
{ kind: StripItemKind.Text, text: '25 of 500 exceptions' },
]);
});
it('still says n of m when the whole list fits on one page', () => {
renderHook(() => useExceptionsStripInfo({ shownCount: 42, totalCount: 42 }));
expect(useBottomStripStore.getState().left?.[0]).toMatchObject({
text: '42 of 42 exceptions',
});
});
it('says exception, not exceptions, when there is one', () => {
renderHook(() => useExceptionsStripInfo({ shownCount: 1, totalCount: 1 }));
expect(useBottomStripStore.getState().left?.[0]).toMatchObject({
text: '1 of 1 exception',
});
});
it('shows zero before the counts land', () => {
renderHook(() => useExceptionsStripInfo({ shownCount: 0, totalCount: 0 }));
expect(useBottomStripStore.getState().left?.[0]).toMatchObject({
text: '0 of 0 exceptions',
});
});
it('clears the strip when the page unmounts', () => {
const { unmount } = renderHook(() =>
useExceptionsStripInfo({ shownCount: 25, totalCount: 500 }),
);
unmount();
expect(useBottomStripStore.getState().left).toBeNull();
});
});

View File

@@ -38,6 +38,7 @@ import { Exception, PayloadProps } from 'types/api/errors/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { FilterDropdownExtendsProps } from './types';
import { useExceptionsStripInfo } from './useExceptionsStripInfo';
import {
extractFilterValues,
getDefaultFilterValue,
@@ -160,6 +161,11 @@ function AllErrors(): JSX.Element {
},
]);
useExceptionsStripInfo({
shownCount: data?.payload?.length ?? 0,
totalCount: errorCountResponse.data?.payload ?? 0,
});
const isFetching = isErrorsFetching || errorCountResponse.isFetching;
useEffect(() => {
setIsFetching(isFetching);

View File

@@ -0,0 +1,26 @@
import { useMemo } from 'react';
import { useBottomStrip } from 'container/BottomStrip/useBottomStrip';
import { type StripItem, StripItemKind } from 'container/BottomStrip/types';
import { pluralize } from 'utils/pluralize';
interface UseExceptionsStripInfoArgs {
shownCount: number;
totalCount: number;
}
export function useExceptionsStripInfo({
shownCount,
totalCount,
}: UseExceptionsStripInfoArgs): void {
const items = useMemo<StripItem[]>(
() => [
{
kind: StripItemKind.Text,
text: `${shownCount} of ${pluralize(totalCount, 'exception')}`,
},
],
[shownCount, totalCount],
);
useBottomStrip(items);
}

View File

@@ -155,7 +155,7 @@ function DomainList(): JSX.Element {
dataSource={DataSource.TRACES}
queryData={query}
onChange={handleSearchChange}
placeholder="Enter your filter query (e.g., deployment.environment = 'otel-demo' AND service.name = 'frontend')"
placeholder="Enter your filter query (e.g., deployment.environment.name = 'otel-demo' AND service.name = 'frontend')"
hardcodedAttributeKeys={ApiMonitoringHardcodedAttributeKeys}
/>
</div>
@@ -180,9 +180,8 @@ function DomainList(): JSX.Element {
</div>
<div className="no-domain-subtitle">
Ensure all HTTP client spans are being sent with kind as{' '}
<span className="attribute">Client</span> and url set in{' '}
<span className="attribute">url.full</span> or{' '}
<span className="attribute">http.url</span> attribute.
<span className="attribute">Client</span> and the URL set in the{' '}
<span className="attribute">url.full</span> attribute.
</div>
<a
href={DOCLINKS.EXTERNAL_API_MONITORING}

View File

@@ -6,9 +6,9 @@ import { SPAN_ATTRIBUTES } from './Explorer/Domains/DomainDetails/constants';
export const ApiMonitoringHardcodedAttributeKeys: QueryKeyDataSuggestionsProps[] =
[
{
label: 'deployment.environment',
label: 'deployment.environment.name',
type: 'resource',
name: 'deployment.environment',
name: 'deployment.environment.name',
signal: 'traces',
fieldDataType: QUERY_BUILDER_KEY_TYPES.STRING,
},

View File

@@ -87,7 +87,7 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
title: 'Environment',
attributeKey: {
key: 'deployment.environment',
key: 'deployment.environment.name',
dataType: DataTypes.String,
type: 'resource',
},

View File

@@ -854,7 +854,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
<ChangelogModal changelog={changelog} onClose={toggleChangelogModal} />
)}
<Toaster />
<Toaster
offset={{ bottom: 'calc(var(--bottom-strip-height, 0px) + 24px)' }}
/>
</Layout>
</TooltipProvider>
);

View File

@@ -26,6 +26,7 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import { useHomeStripInfo } from 'container/Home/useHomeStripInfo';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
@@ -64,6 +65,8 @@ const homeInterval = 30 * 60 * 1000;
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function Home(): JSX.Element {
useHomeStripInfo();
const { user } = useAppContext();
const { safeNavigate } = useSafeNavigate();
const isDarkMode = useIsDarkMode();

View File

@@ -0,0 +1,58 @@
import { renderHook } from '@testing-library/react';
import { useGetAlerts } from 'api/generated/services/alerts';
import { useBottomStripStore } from 'container/BottomStrip/store/useBottomStripStore';
import { StripItemKind } from 'container/BottomStrip/types';
import { useHomeStripInfo } from '../useHomeStripInfo';
jest.mock('api/generated/services/alerts', () => ({
useGetAlerts: jest.fn(),
}));
const mockUseGetAlerts = useGetAlerts as jest.Mock;
describe('useHomeStripInfo', () => {
beforeEach(() => {
useBottomStripStore.setState({ left: null, ownerId: null });
});
it('counts the firing alert instances', () => {
mockUseGetAlerts.mockReturnValue({ data: { data: [{}, {}, {}] } });
renderHook(() => useHomeStripInfo());
expect(useBottomStripStore.getState().left).toMatchObject([
{ kind: StripItemKind.Text, text: '3 alerts firing' },
]);
});
it('says alert, not alerts, when only one is firing', () => {
mockUseGetAlerts.mockReturnValue({ data: { data: [{}] } });
renderHook(() => useHomeStripInfo());
expect(useBottomStripStore.getState().left?.[0]).toMatchObject({
text: '1 alert firing',
});
});
it('shows zero before the response lands', () => {
mockUseGetAlerts.mockReturnValue({ data: undefined });
renderHook(() => useHomeStripInfo());
expect(useBottomStripStore.getState().left?.[0]).toMatchObject({
text: '0 alerts firing',
});
});
it('clears the strip when the page unmounts', () => {
mockUseGetAlerts.mockReturnValue({ data: { data: [{}, {}, {}] } });
const { unmount } = renderHook(() => useHomeStripInfo());
unmount();
expect(useBottomStripStore.getState().left).toBeNull();
});
});

View File

@@ -0,0 +1,21 @@
import { useMemo } from 'react';
import { useGetAlerts } from 'api/generated/services/alerts';
import { useBottomStrip } from 'container/BottomStrip/useBottomStrip';
import { type StripItem, StripItemKind } from 'container/BottomStrip/types';
import { pluralize } from 'utils/pluralize';
export function useHomeStripInfo(): void {
// Firing instances, not rules, matching the triggered alerts page.
const { data } = useGetAlerts();
const count = data?.data?.length ?? 0;
const items = useMemo<StripItem[]>(
() => [
{ kind: StripItemKind.Text, text: `${pluralize(count, 'alert')} firing` },
],
[count],
);
useBottomStrip(items);
}

View File

@@ -11,6 +11,7 @@ import {
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { FormatTimezoneAdjustedTimestamp } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
import { getSemconvMembers } from 'utils/semconv';
import styles from './traceListColumns.module.scss';
const keyToLabelMap: Record<string, string> = {
@@ -27,7 +28,12 @@ const keyToLabelMap: Record<string, string> = {
const keyAliases: Record<string, string[]> = {
serviceName: ['serviceName', 'service.name', 'service_name'],
durationNano: ['durationNano', 'duration.nano', 'duration_nano'],
httpMethod: ['httpMethod', 'http.method', 'http_method'],
httpMethod: [
'httpMethod',
...getSemconvMembers('http.request.method'),
'http_request_method',
'http_method',
],
responseStatusCode: [
'response_status_code',
'response.status.code',

View File

@@ -128,7 +128,7 @@ export const INFRA_MONITORING_ATTR_KEYS = {
K8S_OBJECT_NAME: 'k8s.object.name',
// Environment
DEPLOYMENT_ENVIRONMENT: 'deployment.environment',
DEPLOYMENT_ENVIRONMENT: 'deployment.environment.name',
// Host System
OS_TYPE: 'os.type',
@@ -907,7 +907,7 @@ export const ENTITY_FILTER_PLACEHOLDERS: Record<InfraMonitoringEntity, string> =
[InfraMonitoringEntity.NAMESPACES]:
"Enter your filter query (e.g., k8s.namespace.name = 'production' AND k8s.cluster.name = 'prod-cluster')",
[InfraMonitoringEntity.CLUSTERS]:
"Enter your filter query (e.g., k8s.cluster.name = 'prod-cluster' AND deployment.environment = 'production')",
"Enter your filter query (e.g., k8s.cluster.name = 'prod-cluster' AND deployment.environment.name = 'production')",
[InfraMonitoringEntity.DEPLOYMENTS]:
"Enter your filter query (e.g., k8s.deployment.name = 'api-server' AND k8s.namespace.name = 'production')",
[InfraMonitoringEntity.STATEFULSETS]:

View File

@@ -2,6 +2,17 @@
color: white;
}
.semconv-migration-report {
margin-top: 32px;
display: flex;
flex-direction: column;
gap: 12px;
.ant-table-wrapper {
margin-top: 4px;
}
}
.ingestion-key-container {
margin-top: 24px;
display: flex;

View File

@@ -5,6 +5,8 @@ import getIngestionData from 'api/settings/getIngestionData';
import { useAppContext } from 'providers/App/App';
import { IngestionDataType } from 'types/api/settings/ingestion';
import SemconvMigrationReport from './SemconvMigrationReport';
import './IngestionSettings.styles.scss';
export default function IngestionSettings(): JSX.Element {
@@ -84,6 +86,7 @@ export default function IngestionSettings(): JSX.Element {
dataSource={data}
bordered
/>
<SemconvMigrationReport />
</div>
);
}

View File

@@ -83,6 +83,8 @@ import { MeterAggregateOperator } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { getDaysUntilExpiry } from 'utils/timeUtils';
import SemconvMigrationReport from './SemconvMigrationReport';
import './IngestionSettings.styles.scss';
const { Option } = Select;
@@ -1705,6 +1707,7 @@ function MultiIngestionSettings(): JSX.Element {
}}
className="ingestion-keys-table"
/>
<SemconvMigrationReport />
</div>
{/* Delete Key Modal */}

View File

@@ -0,0 +1,72 @@
import { useQuery } from 'react-query';
import { Alert, Table, TableColumnsType } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import getSemconvMigrationReport from 'api/semconv/getMigrationReport';
import dayjs from 'dayjs';
import { SemconvMigrationReportEntry } from 'types/api/semconvMigration';
function SemconvMigrationReport(): JSX.Element {
const { data, isLoading, isError } = useQuery({
queryKey: ['semconv-migration-report'],
queryFn: getSemconvMigrationReport,
});
const columns: TableColumnsType<SemconvMigrationReportEntry> = [
{
title: 'Old name',
dataIndex: 'old',
key: 'old',
},
{
title: 'Current name',
dataIndex: 'current',
key: 'current',
},
{
title: 'Signal',
dataIndex: 'signal',
key: 'signal',
},
{
title: 'Services still sending only the old name',
dataIndex: 'services',
key: 'services',
render: (services: string[]): string => services.join(', '),
},
{
title: 'Last seen',
dataIndex: 'lastSeenUnixMilli',
key: 'lastSeenUnixMilli',
render: (value: number): string =>
dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
},
];
return (
<section className="semconv-migration-report">
<Typography.Title level={4}>Semantic convention migration</Typography.Title>
<Typography.Text>
Services in this report sent an old OpenTelemetry field during the last 24
hours without sending its current replacement. Update their SDK or
instrumentation when practical; SigNoz queries remain backward compatible.
</Typography.Text>
{isError && (
<Alert
type="error"
showIcon
message="Could not load the semantic convention migration report"
/>
)}
<Table
loading={isLoading}
columns={columns}
dataSource={data?.entries ?? []}
rowKey={(entry): string => `${entry.current}-${entry.old}-${entry.signal}`}
pagination={false}
locale={{ emptyText: 'No old-only services found in the last 24 hours' }}
/>
</section>
);
}
export default SemconvMigrationReport;

View File

@@ -18,7 +18,7 @@ export const SAMPLE_SPAN_JSON = `{
},
"resource": {
"service.name": "llm-gateway",
"deployment.environment": "production"
"deployment.environment.name": "production"
}
}`;

View File

@@ -1,3 +1,5 @@
import { getSemconvMembers } from 'utils/semconv';
// Field-name allowlists that drive signal-specific cell rendering. Both legacy
// camelCase and snake_case variants are listed because the API has shipped both.
@@ -12,12 +14,10 @@ export const TIMESTAMP_FIELD_NAMES = new Set([
export const STATUS_FIELD_NAMES = new Set([
'httpMethod',
'http_method',
'http.method',
'http.request.method',
...getSemconvMembers('http.request.method'),
'responseStatusCode',
'response_status_code',
'http.status_code',
'http.response.status_code',
...getSemconvMembers('http.response.status_code'),
]);
// trace_/max_llm_duration_nano are trace-level durations the per-trace query computes.

View File

@@ -74,7 +74,8 @@ export function tracesRunQueryAction(
properties: {
key: {
type: 'string',
description: 'Attribute key, e.g. service.name, http.status_code',
description:
'Attribute key, e.g. service.name, http.response.status_code',
},
op: {
type: 'string',
@@ -143,7 +144,7 @@ export function tracesAddFilterAction(
properties: {
key: {
type: 'string',
description: 'Attribute key, e.g. service.name, http.status_code',
description: 'Attribute key, e.g. service.name, http.response.status_code',
},
op: {
type: 'string',

View File

@@ -0,0 +1,53 @@
import { renderHook } from '@testing-library/react';
import { useBottomStripStore } from 'container/BottomStrip/store/useBottomStripStore';
import { StripItemKind } from 'container/BottomStrip/types';
import { useAlertRulesStripInfo } from '../useAlertRulesStripInfo';
describe('useAlertRulesStripInfo', () => {
beforeEach(() => {
useBottomStripStore.setState({ left: null, ownerId: null });
});
it('shows the rows on the page against the total', () => {
renderHook(() => useAlertRulesStripInfo({ shownCount: 15, totalCount: 17 }));
expect(useBottomStripStore.getState().left).toMatchObject([
{ kind: StripItemKind.Text, text: '15 of 17 rules' },
]);
});
it('still says n of m when the whole list fits on one page', () => {
renderHook(() => useAlertRulesStripInfo({ shownCount: 17, totalCount: 17 }));
expect(useBottomStripStore.getState().left?.[0]).toMatchObject({
text: '17 of 17 rules',
});
});
it('says rule, not rules, when there is only one', () => {
renderHook(() => useAlertRulesStripInfo({ shownCount: 1, totalCount: 1 }));
expect(useBottomStripStore.getState().left?.[0]).toMatchObject({
text: '1 of 1 rule',
});
});
it('shows zero when nothing matched', () => {
renderHook(() => useAlertRulesStripInfo({ shownCount: 0, totalCount: 12 }));
expect(useBottomStripStore.getState().left?.[0]).toMatchObject({
text: '0 of 12 rules',
});
});
it('clears the strip when the page unmounts', () => {
const { unmount } = renderHook(() =>
useAlertRulesStripInfo({ shownCount: 15, totalCount: 17 }),
);
unmount();
expect(useBottomStripStore.getState().left).toBeNull();
});
});

View File

@@ -20,6 +20,7 @@ import { ALERT_RULES_PARAMS, useAlertRulesFilters } from './hooks';
import styles from './ListAlertRules.module.scss';
import { getAlertRuleColumns } from './table.config';
import type { AlertRule } from './types';
import { useAlertRulesStripInfo } from './useAlertRulesStripInfo';
import { useAlertRulesData } from './useAlertRulesData';
import { useAlertRulesHandlers } from './useAlertRulesHandlers';
@@ -87,6 +88,11 @@ function ListAlertRules(): JSX.Element {
return filteredRules.slice(start, start + limit);
}, [filteredRules, page, limit]);
useAlertRulesStripInfo({
shownCount: paginatedRules.length,
totalCount: filteredRules.length,
});
const columnsWithActions = useMemo(() => {
if (!action) {
return columns;

View File

@@ -0,0 +1,27 @@
import { useMemo } from 'react';
import { useBottomStrip } from 'container/BottomStrip/useBottomStrip';
import { type StripItem, StripItemKind } from 'container/BottomStrip/types';
import { pluralize } from 'utils/pluralize';
interface UseAlertRulesStripInfoArgs {
/** Rows on the current page, matching the table's own footer. */
shownCount: number;
totalCount: number;
}
export function useAlertRulesStripInfo({
shownCount,
totalCount,
}: UseAlertRulesStripInfoArgs): void {
const items = useMemo<StripItem[]>(
() => [
{
kind: StripItemKind.Text,
text: `${shownCount} of ${pluralize(totalCount, 'rule')}`,
},
],
[shownCount, totalCount],
);
useBottomStrip(items);
}

View File

@@ -1,6 +1,7 @@
import { Divider } from '@signozhq/ui/divider';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { SemconvOldNameBadge } from 'components/Semconv';
import { TagContainer, TagLabel, TagValue } from './FieldRenderer.styles';
import { FieldRendererProps } from './LogDetailedView.types';
@@ -28,6 +29,7 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
<Typography.Text truncate={1} className="label">
{newField}{' '}
</Typography.Text>
<SemconvOldNameBadge name={newField} />
</TooltipSimple>
<div className="tags">
@@ -47,7 +49,10 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
</div>
</>
) : (
<span className="label">{field}</span>
<>
<span className="label">{field}</span>
<SemconvOldNameBadge name={field} />
</>
)}
</span>
);

View File

@@ -164,7 +164,9 @@ describe('useInitialQuery - Priority-Based Resource Filtering', () => {
value: 'frontend-service',
}),
expect.objectContaining({
key: expect.objectContaining({ key: 'deployment.environment' }),
key: expect.objectContaining({
key: 'deployment.environment.name',
}),
value: 'production',
}),
expect.objectContaining({
@@ -286,7 +288,9 @@ describe('useInitialQuery - Priority-Based Resource Filtering', () => {
value: 'legacy-app',
}),
expect.objectContaining({
key: expect.objectContaining({ key: 'deployment.environment' }),
key: expect.objectContaining({
key: 'deployment.environment.name',
}),
value: 'production',
}),
expect.objectContaining({

View File

@@ -6,13 +6,14 @@ import {
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
import { getSemconvRename } from 'utils/semconv';
const FALLBACK_STARTS_WITH_REGEX = /^(k8s|cloud|host|deployment)/; // regex to filter out resources that start with the specified keywords
const FALLBACK_CONTAINS_REGEX = /(env|service|file|container|tenant)/; // regex to filter out resources that contains the specified keywords
// Priority categories for filter selection
// Strategy:
// - Always include: service.name, deployment.environment, env, environment
// - Always include: service.name, deployment.environment.name, env, environment
// - Select ONE category only: stops at the first category with a matching attribute
// - Within category: picks the first available attribute by order
// - Order (highest to lowest priority): Kubernetes > Cloud > Host > Container
@@ -26,27 +27,36 @@ const PRIORITY_CATEGORIES = [
const SERVICE_AND_ENVIRONMENT_KEYS = [
'service.name',
'deployment.environment',
'deployment.environment.name',
'env',
'environment',
];
export const getFiltersFromResources = (
resources: ILog['resources_string'],
): TagFilterItem[] =>
Object.keys(resources).map((key: string) => {
): TagFilterItem[] => {
const items = new Map<string, TagFilterItem>();
Object.keys(resources).forEach((key: string) => {
const currentKey = getSemconvRename(key)?.current ?? key;
const resourceValue = resources[key] as string;
return {
const item = {
id: uuid(),
key: {
key,
key: currentKey,
dataType: DataTypes.String,
type: 'resource',
},
op: OPERATORS['='],
value: resourceValue,
};
// If raw data contains both names, retain the current value just like the
// backend's current-first resolver.
if (!items.has(currentKey) || key === currentKey) {
items.set(currentKey, item);
}
});
return Array.from(items.values());
};
export const isServiceOrEnvironmentAttribute = (key: string): boolean =>
SERVICE_AND_ENVIRONMENT_KEYS.includes(key);

View File

@@ -28,7 +28,7 @@ import { v4 as uuid } from 'uuid';
export const dbSystemTags: Tags[] = [
{
Key: 'db.system.(string)',
Key: 'db.system.name.(string)',
StringValues: [''],
NumberValues: [],
BoolValues: [],

View File

@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next';
import { Form } from 'antd';
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
import { SemconvEditorWarning } from 'components/Semconv';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import isEqual from 'lodash-es/isEqual';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
@@ -55,6 +56,7 @@ function TagFilterInputWithLogsResultPreview({
value={value}
onChange={onChange}
/>
<SemconvEditorWarning value={value} editor="Pipeline filter" />
<div className="pipeline-filter-input-preview-container">
<LogsFilterPreview filter={value} />
</div>

View File

@@ -2,6 +2,7 @@ import { ChangeEvent, useCallback } from 'react';
import MEditor, { Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Input } from 'antd';
import { SemconvEditorWarning } from 'components/Semconv';
import { LEGEND } from 'constants/global';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -118,6 +119,7 @@ function ClickHouseQueryBuilder({
theme={isDarkMode ? 'my-theme' : 'light'}
beforeMount={setEditorTheme}
/>
<SemconvEditorWarning value={queryData?.query} editor="ClickHouse SQL" />
<Input
onChange={handleUpdateInput}
name="legend"

View File

@@ -1,6 +1,7 @@
import { ChangeEvent, useCallback } from 'react';
import { Input } from 'antd';
import { LEGEND } from 'constants/global';
import { SemconvEditorWarning } from 'components/Semconv';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { IPromQLQuery } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
@@ -66,6 +67,7 @@ function PromQLQueryBuilder({
style={{ marginBottom: '0.5rem' }}
data-testid="promql-query-input"
/>
<SemconvEditorWarning value={queryData?.query} editor="PromQL" />
<Input
onChange={handleUpdateQuery}

View File

@@ -21,6 +21,7 @@ import { getTotalRPS } from 'utils/services';
import { getColumns } from '../Columns/ServiceColumn';
import { ServiceMetricsTableProps } from '../types';
import { useServicesStripInfo } from '../useServicesStripInfo';
import { getServiceListFromQuery } from '../utils';
function ServiceMetricTable({
@@ -67,6 +68,8 @@ function ServiceMetricTable({
[isLoading, queries, topLevelOperations],
);
useServicesStripInfo(services.length);
const { search } = useLocation();
const tableColumns = useMemo(() => getColumns(search, true), [search]);
const [RPS, setRPS] = useState(0);

View File

@@ -18,6 +18,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'hooks/useResourceAttribute/types';
import SkipOnBoardingModal from '../SkipOnBoardModal';
import { useServicesStripInfo } from '../useServicesStripInfo';
import ServiceTraceTable from './ServiceTracesTable';
function ServiceTraces(): JSX.Element {
@@ -42,6 +43,8 @@ function ServiceTraces(): JSX.Element {
const services = data || [];
useServicesStripInfo(services.length);
const [skipOnboarding, setSkipOnboarding] = useState(
localStorageGet(SKIP_ONBOARDING) === 'true',
);

View File

@@ -0,0 +1,43 @@
import { renderHook } from '@testing-library/react';
import { useBottomStripStore } from 'container/BottomStrip/store/useBottomStripStore';
import { StripItemKind } from 'container/BottomStrip/types';
import { useServicesStripInfo } from '../useServicesStripInfo';
describe('useServicesStripInfo', () => {
beforeEach(() => {
useBottomStripStore.setState({ left: null, ownerId: null });
});
it('shows how many services are listed', () => {
renderHook(() => useServicesStripInfo(18));
expect(useBottomStripStore.getState().left).toMatchObject([
{ kind: StripItemKind.Text, text: '18 services' },
]);
});
it('says service, not services, when there is one', () => {
renderHook(() => useServicesStripInfo(1));
expect(useBottomStripStore.getState().left?.[0]).toMatchObject({
text: '1 service',
});
});
it('shows zero when there are none', () => {
renderHook(() => useServicesStripInfo(0));
expect(useBottomStripStore.getState().left?.[0]).toMatchObject({
text: '0 services',
});
});
it('clears the strip when the page unmounts', () => {
const { unmount } = renderHook(() => useServicesStripInfo(18));
unmount();
expect(useBottomStripStore.getState().left).toBeNull();
});
});

View File

@@ -0,0 +1,13 @@
import { useMemo } from 'react';
import { useBottomStrip } from 'container/BottomStrip/useBottomStrip';
import { type StripItem, StripItemKind } from 'container/BottomStrip/types';
import { pluralize } from 'utils/pluralize';
export function useServicesStripInfo(count: number): void {
const items = useMemo<StripItem[]>(
() => [{ kind: StripItemKind.Text, text: pluralize(count, 'service') }],
[count],
);
useBottomStrip(items);
}

View File

@@ -1,3 +1,5 @@
import { getSemconvMembers } from 'utils/semconv';
// Field-name allowlists that drive signal-specific cell rendering. Both legacy
// camelCase and snake_case variants are listed because the API has shipped both.
export const TIMESTAMP_FIELD_NAMES = new Set(['timestamp']);
@@ -5,12 +7,10 @@ export const TIMESTAMP_FIELD_NAMES = new Set(['timestamp']);
export const STATUS_FIELD_NAMES = new Set([
'httpMethod',
'http_method',
'http.method',
'http.request.method',
...getSemconvMembers('http.request.method'),
'responseStatusCode',
'response_status_code',
'http.status_code',
'http.response.status_code',
...getSemconvMembers('http.response.status_code'),
]);
export const DURATION_FIELD_NAMES = new Set(['durationNano', 'duration_nano']);

View File

@@ -11,6 +11,8 @@ import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantS
import { VariantContext } from 'container/AIAssistant/VariantContext';
import Noz from 'components/Noz/Noz';
import { useAIAssistantStripInfo } from './useAIAssistantStripInfo';
import styles from './AIAssistantPage.module.scss';
import ConversationsList from 'container/AIAssistant/components/ConversationsList';
@@ -41,6 +43,8 @@ export default function AIAssistantPage(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useAIAssistantStripInfo();
const conversations = useAIAssistantStore((s) => s.conversations);
const activeConversationId = useAIAssistantStore(
(s) => s.activeConversationId,

View File

@@ -0,0 +1,60 @@
import { renderHook } from '@testing-library/react';
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
import { useBottomStripStore } from 'container/BottomStrip/store/useBottomStripStore';
import { StripItemKind } from 'container/BottomStrip/types';
import { useAIAssistantStripInfo } from '../useAIAssistantStripInfo';
function seed(conversations: Record<string, unknown>): void {
useAIAssistantStore.setState({ conversations } as never);
}
describe('useAIAssistantStripInfo', () => {
beforeEach(() => {
useBottomStripStore.setState({ left: null, ownerId: null });
});
it('counts only the conversations that are not archived', () => {
seed({
a: { id: 'a' },
b: { id: 'b' },
c: { id: 'c', archived: true },
});
renderHook(() => useAIAssistantStripInfo());
expect(useBottomStripStore.getState().left).toMatchObject([
{ kind: StripItemKind.Text, text: '2 conversations' },
]);
});
it('says one conversation, not 1 conversations', () => {
seed({ a: { id: 'a' } });
renderHook(() => useAIAssistantStripInfo());
expect(useBottomStripStore.getState().left?.[0]).toMatchObject({
text: '1 conversation',
});
});
it('shows zero when there are none', () => {
seed({});
renderHook(() => useAIAssistantStripInfo());
expect(useBottomStripStore.getState().left?.[0]).toMatchObject({
text: '0 conversations',
});
});
it('clears the strip when the page unmounts', () => {
seed({ a: { id: 'a' } });
const { unmount } = renderHook(() => useAIAssistantStripInfo());
unmount();
expect(useBottomStripStore.getState().left).toBeNull();
});
});

View File

@@ -243,3 +243,11 @@ export const TooltipsInApprovalDiff: Story = {
args: { tooltipsOpen: true, agent: 'awaiting-approval', contents: BRIEF },
play: openApprovalDiff,
};
/**
* The conversation count in the bottom strip, in place of the build version.
* Archived threads are left out of it.
*/
export const BottomStrip: Story = {
args: { bottomStrip: true },
};

View File

@@ -0,0 +1,20 @@
import { useMemo } from 'react';
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
import { useBottomStrip } from 'container/BottomStrip/useBottomStrip';
import { type StripItem, StripItemKind } from 'container/BottomStrip/types';
import { pluralize } from 'utils/pluralize';
export function useAIAssistantStripInfo(): void {
const conversations = useAIAssistantStore((state) => state.conversations);
const count = Object.values(conversations).filter(
(conversation) => !conversation.archived,
).length;
const items = useMemo<StripItem[]>(
() => [{ kind: StripItemKind.Text, text: pluralize(count, 'conversation') }],
[count],
);
useBottomStrip(items);
}

View File

@@ -133,3 +133,19 @@ export const ColumnPicker: Story = {
export const Tooltips: Story = {
args: { tooltipsOpen: true },
};
/**
* The rule count in the bottom strip, in place of the build version: the rows on
* the page against the total, the same pair the table's own footer prints.
*/
export const BottomStrip: Story = {
args: { bottomStrip: true },
};
/** The same count on a second page, where the two numbers come apart. */
export const BottomStripPaginated: Story = {
args: { bottomStrip: true, rules: RULE_MAX },
parameters: {
signoz: { route: '/alerts?tab=AlertRules&page=2&limit=10' },
},
};

View File

@@ -125,3 +125,11 @@ export const QuickFiltersSettingsWithBanner: Story = {
args: { banner: 'trial-expiry' },
play: dirtyQuickFiltersSettings,
};
/**
* The exception count in the bottom strip, in place of the build version: the
* rows on the page against the total the count query returns.
*/
export const BottomStrip: Story = {
args: { bottomStrip: true },
};

View File

@@ -35,13 +35,7 @@ export default function CeleryOverviewDetails({
? undefined
: getFiltersFromKeyValue('messaging.system', value, 'tag');
case 'destination':
return getFiltersFromKeyValue(
details.messaging_system === 'celery'
? 'messaging.destination'
: 'messaging.destination.name',
value,
'tag',
);
return getFiltersFromKeyValue('messaging.destination.name', value, 'tag');
case 'kind_string':
return getFiltersFromKeyValue('kind_string', value, '');
default:

View File

@@ -97,3 +97,11 @@ export const NavSettingsMenu: Story = {
await screen.findByRole('menu');
},
};
/**
* The firing alert count in the bottom strip, in place of the build version. It
* counts firing instances, so it does not match the alert rules widget above.
*/
export const BottomStrip: Story = {
args: { bottomStrip: true },
};

View File

@@ -233,7 +233,7 @@ describe('Logs Explorer Tests', () => {
);
const queries = queryAllByText(
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')",
"Enter your filter query (e.g., http.response.status_code >= 500 AND service.name = 'frontend')",
);
expect(queries).toHaveLength(1);
});

View File

@@ -40,7 +40,7 @@ export const LogsQuickFiltersConfig: IQuickFiltersConfig[] = [
type: FiltersType.CHECKBOX,
title: 'Environment',
attributeKey: {
key: 'deployment.environment',
key: 'deployment.environment.name',
dataType: DataTypes.String,
type: 'resource',
},

View File

@@ -61,3 +61,13 @@ export const Pagination: Story = {
export const OverTrialLimit: Story = {
args: { traffic: 'over-trial-limit', banner: 'trial-expiry' },
};
/** The service count in the bottom strip, in place of the build version. */
export const BottomStrip: Story = {
args: { bottomStrip: true },
};
/** The same count on the span metrics table, which is the page's other path. */
export const BottomStripSpanMetrics: Story = {
args: { bottomStrip: true, mode: 'span-metrics' },
};

View File

@@ -11,6 +11,7 @@ import { Skeleton } from 'antd';
import { DetailsHeader, DetailsPanelDrawer } from 'components/DetailsPanel';
import { HeaderAction } from 'components/DetailsPanel/DetailsHeader/DetailsHeader';
import { DetailsPanelState } from 'components/DetailsPanel/types';
import { SemconvOldNameBadge } from 'components/Semconv';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
@@ -108,6 +109,12 @@ function SpanDetailsContent({
() => getSpanDisplayData(selectedSpan),
[selectedSpan],
);
const semconvLabelSuffix = useCallback(
(fieldKey: string): React.ReactNode => (
<SemconvOldNameBadge name={fieldKey} />
),
[],
);
// Map span attribute actions to PrettyView actions format.
// Use the last key in fieldKeyPath (the actual attribute key), not the full display path.
@@ -329,6 +336,7 @@ function SpanDetailsContent({
visibleActions: VISIBLE_ACTIONS,
pinnedFieldsValue,
onPinnedFieldsChange,
labelSuffixRenderer: semconvLabelSuffix,
}}
/>
</TabsContent>

View File

@@ -29,7 +29,7 @@ export const KEY_ATTRIBUTE_KEYS: Record<string, string[]> = {
traces: [
'service.name',
'service.namespace',
'deployment.environment',
'deployment.environment.name',
'timestamp',
'duration_nano',
'kind_string',

View File

@@ -359,7 +359,7 @@ function Filters({
onChange={handleExpressionChange}
onRun={handleRunQuery}
dataSource={DataSource.TRACES}
placeholder="Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')"
placeholder="Enter your filter query (e.g., http.response.status_code >= 500 AND service.name = 'frontend')"
/>
</div>
</div>

View File

@@ -22,8 +22,8 @@ export const SPAN_CATEGORIES: readonly SpanCategory[] = [
// Map each category to the attribute key it filters on
const CATEGORY_KEYS: Record<Exclude<SpanCategory, 'All'>, string> = {
Database: 'db.system',
HTTP: 'http.method',
Database: 'db.system.name',
HTTP: 'http.request.method',
Functions: 'kind_string',
Jobs: 'messaging.system',
LLM: 'gen_ai.request.model',
@@ -34,8 +34,8 @@ const ALL_CATEGORY_KEYS = Object.values(CATEGORY_KEYS);
// The expression clause to add for each category
const CATEGORY_EXPRESSIONS: Record<Exclude<SpanCategory, 'All'>, string> = {
Database: 'db.system exists',
HTTP: 'http.method exists',
Database: 'db.system.name exists',
HTTP: 'http.request.method exists',
Functions: "kind_string = 'Internal'",
Jobs: 'messaging.system exists',
LLM: 'gen_ai.request.model exists',

View File

@@ -74,7 +74,8 @@ export function tracesRunQueryAction(
properties: {
key: {
type: 'string',
description: 'Attribute key, e.g. service.name, http.status_code',
description:
'Attribute key, e.g. service.name, http.response.status_code',
},
op: {
type: 'string',
@@ -143,7 +144,7 @@ export function tracesAddFilterAction(
properties: {
key: {
type: 'string',
description: 'Attribute key, e.g. service.name, http.status_code',
description: 'Attribute key, e.g. service.name, http.response.status_code',
},
op: {
type: 'string',

File diff suppressed because one or more lines are too long

View File

@@ -25,12 +25,13 @@ HAS=24
HASANY=25
HASALL=26
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
EXACT=28
BOOL=29
NUMBER=30
QUOTED_TEXT=31
KEY=32
WS=33
FREETEXT=34
'('=1
')'=2
'['=3

File diff suppressed because one or more lines are too long

View File

@@ -25,12 +25,13 @@ HAS=24
HASANY=25
HASALL=26
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
EXACT=28
BOOL=29
NUMBER=30
QUOTED_TEXT=31
KEY=32
WS=33
FREETEXT=34
'('=1
')'=2
'['=3

View File

@@ -39,12 +39,13 @@ export default class FilterQueryLexer extends Lexer {
public static readonly HASANY = 25;
public static readonly HASALL = 26;
public static readonly SEARCH = 27;
public static readonly BOOL = 28;
public static readonly NUMBER = 29;
public static readonly QUOTED_TEXT = 30;
public static readonly KEY = 31;
public static readonly WS = 32;
public static readonly FREETEXT = 33;
public static readonly EXACT = 28;
public static readonly BOOL = 29;
public static readonly NUMBER = 30;
public static readonly QUOTED_TEXT = 31;
public static readonly KEY = 32;
public static readonly WS = 33;
public static readonly FREETEXT = 34;
public static readonly EOF = Token.EOF;
public static readonly channelNames: string[] = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ];
@@ -70,8 +71,8 @@ export default class FilterQueryLexer extends Lexer {
"HASTOKEN",
"HAS", "HASANY",
"HASALL", "SEARCH",
"BOOL", "NUMBER",
"QUOTED_TEXT",
"EXACT", "BOOL",
"NUMBER", "QUOTED_TEXT",
"KEY", "WS",
"FREETEXT" ];
public static readonly modeNames: string[] = [ "DEFAULT_MODE", ];
@@ -80,8 +81,8 @@ export default class FilterQueryLexer extends Lexer {
"LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS", "REGEXP",
"CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY", "HASALL",
"SEARCH", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS",
"OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
"SEARCH", "EXACT", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT",
"EMPTY_BRACKS", "OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
];
@@ -102,122 +103,124 @@ export default class FilterQueryLexer extends Lexer {
public get modeNames(): string[] { return FilterQueryLexer.modeNames; }
public static readonly _serializedATN: number[] = [4,0,33,329,6,-1,2,0,
public static readonly _serializedATN: number[] = [4,0,34,337,6,-1,2,0,
7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,
7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,
16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,
2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,
31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,1,0,
1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,3,5,91,8,5,1,6,1,6,1,6,
1,7,1,7,1,7,1,8,1,8,1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,12,1,12,1,12,
1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,
14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,3,15,134,8,15,1,16,1,16,1,16,1,16,
1,16,1,16,1,16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,151,8,17,1,
18,1,18,1,18,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1,22,
1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,24,1,24,1,
24,1,24,1,24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,26,1,26,
1,26,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,210,
8,27,1,28,1,28,1,29,3,29,215,8,29,1,29,4,29,218,8,29,11,29,12,29,219,1,
29,1,29,5,29,224,8,29,10,29,12,29,227,9,29,3,29,229,8,29,1,29,1,29,3,29,
233,8,29,1,29,4,29,236,8,29,11,29,12,29,237,3,29,240,8,29,1,29,3,29,243,
8,29,1,29,1,29,4,29,247,8,29,11,29,12,29,248,1,29,1,29,3,29,253,8,29,1,
29,4,29,256,8,29,11,29,12,29,257,3,29,260,8,29,3,29,262,8,29,1,30,1,30,
1,30,1,30,5,30,268,8,30,10,30,12,30,271,9,30,1,30,1,30,1,30,1,30,1,30,5,
30,278,8,30,10,30,12,30,281,9,30,1,30,3,30,284,8,30,1,31,1,31,5,31,288,
8,31,10,31,12,31,291,9,31,1,32,1,32,1,32,1,33,1,33,1,33,1,33,1,34,1,34,
1,34,1,34,1,34,1,34,1,34,4,34,307,8,34,11,34,12,34,308,5,34,311,8,34,10,
34,12,34,314,9,34,1,35,4,35,317,8,35,11,35,12,35,318,1,35,1,35,1,36,1,36,
1,37,4,37,326,8,37,11,37,12,37,327,0,0,38,1,1,3,2,5,3,7,4,9,5,11,6,13,7,
15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,16,33,17,35,18,37,19,39,
20,41,21,43,22,45,23,47,24,49,25,51,26,53,27,55,28,57,0,59,29,61,30,63,
0,65,0,67,0,69,31,71,32,73,0,75,33,1,0,29,2,0,76,76,108,108,2,0,73,73,105,
105,2,0,75,75,107,107,2,0,69,69,101,101,2,0,66,66,98,98,2,0,84,84,116,116,
2,0,87,87,119,119,2,0,78,78,110,110,2,0,88,88,120,120,2,0,83,83,115,115,
2,0,82,82,114,114,2,0,71,71,103,103,2,0,80,80,112,112,2,0,67,67,99,99,2,
0,79,79,111,111,2,0,65,65,97,97,2,0,68,68,100,100,2,0,72,72,104,104,2,0,
89,89,121,121,2,0,85,85,117,117,2,0,70,70,102,102,2,0,43,43,45,45,2,0,34,
34,92,92,2,0,39,39,92,92,4,0,35,36,64,90,95,95,97,123,7,0,35,36,45,45,47,
58,64,90,95,95,97,123,125,125,3,0,9,10,13,13,32,32,1,0,48,57,8,0,9,10,13,
13,32,34,39,41,44,44,60,62,91,91,93,93,353,0,1,1,0,0,0,0,3,1,0,0,0,0,5,
1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,
0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,
0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,
0,39,1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,1,
0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,55,1,0,0,0,0,59,1,0,0,0,0,61,1,0,0,0,
0,69,1,0,0,0,0,71,1,0,0,0,0,75,1,0,0,0,1,77,1,0,0,0,3,79,1,0,0,0,5,81,1,
0,0,0,7,83,1,0,0,0,9,85,1,0,0,0,11,90,1,0,0,0,13,92,1,0,0,0,15,95,1,0,0,
0,17,98,1,0,0,0,19,100,1,0,0,0,21,103,1,0,0,0,23,105,1,0,0,0,25,108,1,0,
0,0,27,113,1,0,0,0,29,119,1,0,0,0,31,127,1,0,0,0,33,135,1,0,0,0,35,142,
1,0,0,0,37,152,1,0,0,0,39,155,1,0,0,0,41,159,1,0,0,0,43,163,1,0,0,0,45,
166,1,0,0,0,47,175,1,0,0,0,49,179,1,0,0,0,51,186,1,0,0,0,53,193,1,0,0,0,
55,209,1,0,0,0,57,211,1,0,0,0,59,261,1,0,0,0,61,283,1,0,0,0,63,285,1,0,
0,0,65,292,1,0,0,0,67,295,1,0,0,0,69,299,1,0,0,0,71,316,1,0,0,0,73,322,
1,0,0,0,75,325,1,0,0,0,77,78,5,40,0,0,78,2,1,0,0,0,79,80,5,41,0,0,80,4,
1,0,0,0,81,82,5,91,0,0,82,6,1,0,0,0,83,84,5,93,0,0,84,8,1,0,0,0,85,86,5,
44,0,0,86,10,1,0,0,0,87,91,5,61,0,0,88,89,5,61,0,0,89,91,5,61,0,0,90,87,
1,0,0,0,90,88,1,0,0,0,91,12,1,0,0,0,92,93,5,33,0,0,93,94,5,61,0,0,94,14,
1,0,0,0,95,96,5,60,0,0,96,97,5,62,0,0,97,16,1,0,0,0,98,99,5,60,0,0,99,18,
1,0,0,0,100,101,5,60,0,0,101,102,5,61,0,0,102,20,1,0,0,0,103,104,5,62,0,
0,104,22,1,0,0,0,105,106,5,62,0,0,106,107,5,61,0,0,107,24,1,0,0,0,108,109,
7,0,0,0,109,110,7,1,0,0,110,111,7,2,0,0,111,112,7,3,0,0,112,26,1,0,0,0,
113,114,7,1,0,0,114,115,7,0,0,0,115,116,7,1,0,0,116,117,7,2,0,0,117,118,
7,3,0,0,118,28,1,0,0,0,119,120,7,4,0,0,120,121,7,3,0,0,121,122,7,5,0,0,
122,123,7,6,0,0,123,124,7,3,0,0,124,125,7,3,0,0,125,126,7,7,0,0,126,30,
1,0,0,0,127,128,7,3,0,0,128,129,7,8,0,0,129,130,7,1,0,0,130,131,7,9,0,0,
131,133,7,5,0,0,132,134,7,9,0,0,133,132,1,0,0,0,133,134,1,0,0,0,134,32,
1,0,0,0,135,136,7,10,0,0,136,137,7,3,0,0,137,138,7,11,0,0,138,139,7,3,0,
0,139,140,7,8,0,0,140,141,7,12,0,0,141,34,1,0,0,0,142,143,7,13,0,0,143,
144,7,14,0,0,144,145,7,7,0,0,145,146,7,5,0,0,146,147,7,15,0,0,147,148,7,
1,0,0,148,150,7,7,0,0,149,151,7,9,0,0,150,149,1,0,0,0,150,151,1,0,0,0,151,
36,1,0,0,0,152,153,7,1,0,0,153,154,7,7,0,0,154,38,1,0,0,0,155,156,7,7,0,
0,156,157,7,14,0,0,157,158,7,5,0,0,158,40,1,0,0,0,159,160,7,15,0,0,160,
161,7,7,0,0,161,162,7,16,0,0,162,42,1,0,0,0,163,164,7,14,0,0,164,165,7,
10,0,0,165,44,1,0,0,0,166,167,7,17,0,0,167,168,7,15,0,0,168,169,7,9,0,0,
169,170,7,5,0,0,170,171,7,14,0,0,171,172,7,2,0,0,172,173,7,3,0,0,173,174,
7,7,0,0,174,46,1,0,0,0,175,176,7,17,0,0,176,177,7,15,0,0,177,178,7,9,0,
0,178,48,1,0,0,0,179,180,7,17,0,0,180,181,7,15,0,0,181,182,7,9,0,0,182,
183,7,15,0,0,183,184,7,7,0,0,184,185,7,18,0,0,185,50,1,0,0,0,186,187,7,
17,0,0,187,188,7,15,0,0,188,189,7,9,0,0,189,190,7,15,0,0,190,191,7,0,0,
0,191,192,7,0,0,0,192,52,1,0,0,0,193,194,7,9,0,0,194,195,7,3,0,0,195,196,
7,15,0,0,196,197,7,10,0,0,197,198,7,13,0,0,198,199,7,17,0,0,199,54,1,0,
0,0,200,201,7,5,0,0,201,202,7,10,0,0,202,203,7,19,0,0,203,210,7,3,0,0,204,
205,7,20,0,0,205,206,7,15,0,0,206,207,7,0,0,0,207,208,7,9,0,0,208,210,7,
3,0,0,209,200,1,0,0,0,209,204,1,0,0,0,210,56,1,0,0,0,211,212,7,21,0,0,212,
58,1,0,0,0,213,215,3,57,28,0,214,213,1,0,0,0,214,215,1,0,0,0,215,217,1,
0,0,0,216,218,3,73,36,0,217,216,1,0,0,0,218,219,1,0,0,0,219,217,1,0,0,0,
219,220,1,0,0,0,220,228,1,0,0,0,221,225,5,46,0,0,222,224,3,73,36,0,223,
222,1,0,0,0,224,227,1,0,0,0,225,223,1,0,0,0,225,226,1,0,0,0,226,229,1,0,
0,0,227,225,1,0,0,0,228,221,1,0,0,0,228,229,1,0,0,0,229,239,1,0,0,0,230,
232,7,3,0,0,231,233,3,57,28,0,232,231,1,0,0,0,232,233,1,0,0,0,233,235,1,
0,0,0,234,236,3,73,36,0,235,234,1,0,0,0,236,237,1,0,0,0,237,235,1,0,0,0,
237,238,1,0,0,0,238,240,1,0,0,0,239,230,1,0,0,0,239,240,1,0,0,0,240,262,
1,0,0,0,241,243,3,57,28,0,242,241,1,0,0,0,242,243,1,0,0,0,243,244,1,0,0,
0,244,246,5,46,0,0,245,247,3,73,36,0,246,245,1,0,0,0,247,248,1,0,0,0,248,
246,1,0,0,0,248,249,1,0,0,0,249,259,1,0,0,0,250,252,7,3,0,0,251,253,3,57,
28,0,252,251,1,0,0,0,252,253,1,0,0,0,253,255,1,0,0,0,254,256,3,73,36,0,
255,254,1,0,0,0,256,257,1,0,0,0,257,255,1,0,0,0,257,258,1,0,0,0,258,260,
1,0,0,0,259,250,1,0,0,0,259,260,1,0,0,0,260,262,1,0,0,0,261,214,1,0,0,0,
261,242,1,0,0,0,262,60,1,0,0,0,263,269,5,34,0,0,264,268,8,22,0,0,265,266,
5,92,0,0,266,268,9,0,0,0,267,264,1,0,0,0,267,265,1,0,0,0,268,271,1,0,0,
0,269,267,1,0,0,0,269,270,1,0,0,0,270,272,1,0,0,0,271,269,1,0,0,0,272,284,
5,34,0,0,273,279,5,39,0,0,274,278,8,23,0,0,275,276,5,92,0,0,276,278,9,0,
0,0,277,274,1,0,0,0,277,275,1,0,0,0,278,281,1,0,0,0,279,277,1,0,0,0,279,
280,1,0,0,0,280,282,1,0,0,0,281,279,1,0,0,0,282,284,5,39,0,0,283,263,1,
0,0,0,283,273,1,0,0,0,284,62,1,0,0,0,285,289,7,24,0,0,286,288,7,25,0,0,
287,286,1,0,0,0,288,291,1,0,0,0,289,287,1,0,0,0,289,290,1,0,0,0,290,64,
1,0,0,0,291,289,1,0,0,0,292,293,5,91,0,0,293,294,5,93,0,0,294,66,1,0,0,
0,295,296,5,91,0,0,296,297,5,42,0,0,297,298,5,93,0,0,298,68,1,0,0,0,299,
312,3,63,31,0,300,301,5,46,0,0,301,311,3,63,31,0,302,311,3,65,32,0,303,
311,3,67,33,0,304,306,5,46,0,0,305,307,3,73,36,0,306,305,1,0,0,0,307,308,
1,0,0,0,308,306,1,0,0,0,308,309,1,0,0,0,309,311,1,0,0,0,310,300,1,0,0,0,
310,302,1,0,0,0,310,303,1,0,0,0,310,304,1,0,0,0,311,314,1,0,0,0,312,310,
1,0,0,0,312,313,1,0,0,0,313,70,1,0,0,0,314,312,1,0,0,0,315,317,7,26,0,0,
316,315,1,0,0,0,317,318,1,0,0,0,318,316,1,0,0,0,318,319,1,0,0,0,319,320,
1,0,0,0,320,321,6,35,0,0,321,72,1,0,0,0,322,323,7,27,0,0,323,74,1,0,0,0,
324,326,8,28,0,0,325,324,1,0,0,0,326,327,1,0,0,0,327,325,1,0,0,0,327,328,
1,0,0,0,328,76,1,0,0,0,29,0,90,133,150,209,214,219,225,228,232,237,239,
242,248,252,257,259,261,267,269,277,279,283,289,308,310,312,318,327,1,6,
0,0];
31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38,
7,38,1,0,1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,3,5,93,8,5,1,6,
1,6,1,6,1,7,1,7,1,7,1,8,1,8,1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,12,1,
12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,
1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,3,15,136,8,15,1,16,1,16,1,
16,1,16,1,16,1,16,1,16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,153,
8,17,1,18,1,18,1,18,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,
21,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,24,
1,24,1,24,1,24,1,24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,
26,1,26,1,26,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,28,1,28,1,28,
1,28,1,28,1,28,1,28,1,28,1,28,3,28,218,8,28,1,29,1,29,1,30,3,30,223,8,30,
1,30,4,30,226,8,30,11,30,12,30,227,1,30,1,30,5,30,232,8,30,10,30,12,30,
235,9,30,3,30,237,8,30,1,30,1,30,3,30,241,8,30,1,30,4,30,244,8,30,11,30,
12,30,245,3,30,248,8,30,1,30,3,30,251,8,30,1,30,1,30,4,30,255,8,30,11,30,
12,30,256,1,30,1,30,3,30,261,8,30,1,30,4,30,264,8,30,11,30,12,30,265,3,
30,268,8,30,3,30,270,8,30,1,31,1,31,1,31,1,31,5,31,276,8,31,10,31,12,31,
279,9,31,1,31,1,31,1,31,1,31,1,31,5,31,286,8,31,10,31,12,31,289,9,31,1,
31,3,31,292,8,31,1,32,1,32,5,32,296,8,32,10,32,12,32,299,9,32,1,33,1,33,
1,33,1,34,1,34,1,34,1,34,1,35,1,35,1,35,1,35,1,35,1,35,1,35,4,35,315,8,
35,11,35,12,35,316,5,35,319,8,35,10,35,12,35,322,9,35,1,36,4,36,325,8,36,
11,36,12,36,326,1,36,1,36,1,37,1,37,1,38,4,38,334,8,38,11,38,12,38,335,
0,0,39,1,1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,
27,14,29,15,31,16,33,17,35,18,37,19,39,20,41,21,43,22,45,23,47,24,49,25,
51,26,53,27,55,28,57,29,59,0,61,30,63,31,65,0,67,0,69,0,71,32,73,33,75,
0,77,34,1,0,29,2,0,76,76,108,108,2,0,73,73,105,105,2,0,75,75,107,107,2,
0,69,69,101,101,2,0,66,66,98,98,2,0,84,84,116,116,2,0,87,87,119,119,2,0,
78,78,110,110,2,0,88,88,120,120,2,0,83,83,115,115,2,0,82,82,114,114,2,0,
71,71,103,103,2,0,80,80,112,112,2,0,67,67,99,99,2,0,79,79,111,111,2,0,65,
65,97,97,2,0,68,68,100,100,2,0,72,72,104,104,2,0,89,89,121,121,2,0,85,85,
117,117,2,0,70,70,102,102,2,0,43,43,45,45,2,0,34,34,92,92,2,0,39,39,92,
92,4,0,35,36,64,90,95,95,97,123,7,0,35,36,45,45,47,58,64,90,95,95,97,123,
125,125,3,0,9,10,13,13,32,32,1,0,48,57,8,0,9,10,13,13,32,34,39,41,44,44,
60,62,91,91,93,93,361,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,
9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,
0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,
31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,0,
0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,
53,1,0,0,0,0,55,1,0,0,0,0,57,1,0,0,0,0,61,1,0,0,0,0,63,1,0,0,0,0,71,1,0,
0,0,0,73,1,0,0,0,0,77,1,0,0,0,1,79,1,0,0,0,3,81,1,0,0,0,5,83,1,0,0,0,7,
85,1,0,0,0,9,87,1,0,0,0,11,92,1,0,0,0,13,94,1,0,0,0,15,97,1,0,0,0,17,100,
1,0,0,0,19,102,1,0,0,0,21,105,1,0,0,0,23,107,1,0,0,0,25,110,1,0,0,0,27,
115,1,0,0,0,29,121,1,0,0,0,31,129,1,0,0,0,33,137,1,0,0,0,35,144,1,0,0,0,
37,154,1,0,0,0,39,157,1,0,0,0,41,161,1,0,0,0,43,165,1,0,0,0,45,168,1,0,
0,0,47,177,1,0,0,0,49,181,1,0,0,0,51,188,1,0,0,0,53,195,1,0,0,0,55,202,
1,0,0,0,57,217,1,0,0,0,59,219,1,0,0,0,61,269,1,0,0,0,63,291,1,0,0,0,65,
293,1,0,0,0,67,300,1,0,0,0,69,303,1,0,0,0,71,307,1,0,0,0,73,324,1,0,0,0,
75,330,1,0,0,0,77,333,1,0,0,0,79,80,5,40,0,0,80,2,1,0,0,0,81,82,5,41,0,
0,82,4,1,0,0,0,83,84,5,91,0,0,84,6,1,0,0,0,85,86,5,93,0,0,86,8,1,0,0,0,
87,88,5,44,0,0,88,10,1,0,0,0,89,93,5,61,0,0,90,91,5,61,0,0,91,93,5,61,0,
0,92,89,1,0,0,0,92,90,1,0,0,0,93,12,1,0,0,0,94,95,5,33,0,0,95,96,5,61,0,
0,96,14,1,0,0,0,97,98,5,60,0,0,98,99,5,62,0,0,99,16,1,0,0,0,100,101,5,60,
0,0,101,18,1,0,0,0,102,103,5,60,0,0,103,104,5,61,0,0,104,20,1,0,0,0,105,
106,5,62,0,0,106,22,1,0,0,0,107,108,5,62,0,0,108,109,5,61,0,0,109,24,1,
0,0,0,110,111,7,0,0,0,111,112,7,1,0,0,112,113,7,2,0,0,113,114,7,3,0,0,114,
26,1,0,0,0,115,116,7,1,0,0,116,117,7,0,0,0,117,118,7,1,0,0,118,119,7,2,
0,0,119,120,7,3,0,0,120,28,1,0,0,0,121,122,7,4,0,0,122,123,7,3,0,0,123,
124,7,5,0,0,124,125,7,6,0,0,125,126,7,3,0,0,126,127,7,3,0,0,127,128,7,7,
0,0,128,30,1,0,0,0,129,130,7,3,0,0,130,131,7,8,0,0,131,132,7,1,0,0,132,
133,7,9,0,0,133,135,7,5,0,0,134,136,7,9,0,0,135,134,1,0,0,0,135,136,1,0,
0,0,136,32,1,0,0,0,137,138,7,10,0,0,138,139,7,3,0,0,139,140,7,11,0,0,140,
141,7,3,0,0,141,142,7,8,0,0,142,143,7,12,0,0,143,34,1,0,0,0,144,145,7,13,
0,0,145,146,7,14,0,0,146,147,7,7,0,0,147,148,7,5,0,0,148,149,7,15,0,0,149,
150,7,1,0,0,150,152,7,7,0,0,151,153,7,9,0,0,152,151,1,0,0,0,152,153,1,0,
0,0,153,36,1,0,0,0,154,155,7,1,0,0,155,156,7,7,0,0,156,38,1,0,0,0,157,158,
7,7,0,0,158,159,7,14,0,0,159,160,7,5,0,0,160,40,1,0,0,0,161,162,7,15,0,
0,162,163,7,7,0,0,163,164,7,16,0,0,164,42,1,0,0,0,165,166,7,14,0,0,166,
167,7,10,0,0,167,44,1,0,0,0,168,169,7,17,0,0,169,170,7,15,0,0,170,171,7,
9,0,0,171,172,7,5,0,0,172,173,7,14,0,0,173,174,7,2,0,0,174,175,7,3,0,0,
175,176,7,7,0,0,176,46,1,0,0,0,177,178,7,17,0,0,178,179,7,15,0,0,179,180,
7,9,0,0,180,48,1,0,0,0,181,182,7,17,0,0,182,183,7,15,0,0,183,184,7,9,0,
0,184,185,7,15,0,0,185,186,7,7,0,0,186,187,7,18,0,0,187,50,1,0,0,0,188,
189,7,17,0,0,189,190,7,15,0,0,190,191,7,9,0,0,191,192,7,15,0,0,192,193,
7,0,0,0,193,194,7,0,0,0,194,52,1,0,0,0,195,196,7,9,0,0,196,197,7,3,0,0,
197,198,7,15,0,0,198,199,7,10,0,0,199,200,7,13,0,0,200,201,7,17,0,0,201,
54,1,0,0,0,202,203,7,3,0,0,203,204,7,8,0,0,204,205,7,15,0,0,205,206,7,13,
0,0,206,207,7,5,0,0,207,56,1,0,0,0,208,209,7,5,0,0,209,210,7,10,0,0,210,
211,7,19,0,0,211,218,7,3,0,0,212,213,7,20,0,0,213,214,7,15,0,0,214,215,
7,0,0,0,215,216,7,9,0,0,216,218,7,3,0,0,217,208,1,0,0,0,217,212,1,0,0,0,
218,58,1,0,0,0,219,220,7,21,0,0,220,60,1,0,0,0,221,223,3,59,29,0,222,221,
1,0,0,0,222,223,1,0,0,0,223,225,1,0,0,0,224,226,3,75,37,0,225,224,1,0,0,
0,226,227,1,0,0,0,227,225,1,0,0,0,227,228,1,0,0,0,228,236,1,0,0,0,229,233,
5,46,0,0,230,232,3,75,37,0,231,230,1,0,0,0,232,235,1,0,0,0,233,231,1,0,
0,0,233,234,1,0,0,0,234,237,1,0,0,0,235,233,1,0,0,0,236,229,1,0,0,0,236,
237,1,0,0,0,237,247,1,0,0,0,238,240,7,3,0,0,239,241,3,59,29,0,240,239,1,
0,0,0,240,241,1,0,0,0,241,243,1,0,0,0,242,244,3,75,37,0,243,242,1,0,0,0,
244,245,1,0,0,0,245,243,1,0,0,0,245,246,1,0,0,0,246,248,1,0,0,0,247,238,
1,0,0,0,247,248,1,0,0,0,248,270,1,0,0,0,249,251,3,59,29,0,250,249,1,0,0,
0,250,251,1,0,0,0,251,252,1,0,0,0,252,254,5,46,0,0,253,255,3,75,37,0,254,
253,1,0,0,0,255,256,1,0,0,0,256,254,1,0,0,0,256,257,1,0,0,0,257,267,1,0,
0,0,258,260,7,3,0,0,259,261,3,59,29,0,260,259,1,0,0,0,260,261,1,0,0,0,261,
263,1,0,0,0,262,264,3,75,37,0,263,262,1,0,0,0,264,265,1,0,0,0,265,263,1,
0,0,0,265,266,1,0,0,0,266,268,1,0,0,0,267,258,1,0,0,0,267,268,1,0,0,0,268,
270,1,0,0,0,269,222,1,0,0,0,269,250,1,0,0,0,270,62,1,0,0,0,271,277,5,34,
0,0,272,276,8,22,0,0,273,274,5,92,0,0,274,276,9,0,0,0,275,272,1,0,0,0,275,
273,1,0,0,0,276,279,1,0,0,0,277,275,1,0,0,0,277,278,1,0,0,0,278,280,1,0,
0,0,279,277,1,0,0,0,280,292,5,34,0,0,281,287,5,39,0,0,282,286,8,23,0,0,
283,284,5,92,0,0,284,286,9,0,0,0,285,282,1,0,0,0,285,283,1,0,0,0,286,289,
1,0,0,0,287,285,1,0,0,0,287,288,1,0,0,0,288,290,1,0,0,0,289,287,1,0,0,0,
290,292,5,39,0,0,291,271,1,0,0,0,291,281,1,0,0,0,292,64,1,0,0,0,293,297,
7,24,0,0,294,296,7,25,0,0,295,294,1,0,0,0,296,299,1,0,0,0,297,295,1,0,0,
0,297,298,1,0,0,0,298,66,1,0,0,0,299,297,1,0,0,0,300,301,5,91,0,0,301,302,
5,93,0,0,302,68,1,0,0,0,303,304,5,91,0,0,304,305,5,42,0,0,305,306,5,93,
0,0,306,70,1,0,0,0,307,320,3,65,32,0,308,309,5,46,0,0,309,319,3,65,32,0,
310,319,3,67,33,0,311,319,3,69,34,0,312,314,5,46,0,0,313,315,3,75,37,0,
314,313,1,0,0,0,315,316,1,0,0,0,316,314,1,0,0,0,316,317,1,0,0,0,317,319,
1,0,0,0,318,308,1,0,0,0,318,310,1,0,0,0,318,311,1,0,0,0,318,312,1,0,0,0,
319,322,1,0,0,0,320,318,1,0,0,0,320,321,1,0,0,0,321,72,1,0,0,0,322,320,
1,0,0,0,323,325,7,26,0,0,324,323,1,0,0,0,325,326,1,0,0,0,326,324,1,0,0,
0,326,327,1,0,0,0,327,328,1,0,0,0,328,329,6,36,0,0,329,74,1,0,0,0,330,331,
7,27,0,0,331,76,1,0,0,0,332,334,8,28,0,0,333,332,1,0,0,0,334,335,1,0,0,
0,335,333,1,0,0,0,335,336,1,0,0,0,336,78,1,0,0,0,29,0,92,135,152,217,222,
227,233,236,240,245,247,250,256,260,265,267,269,275,277,285,287,291,297,
316,318,320,326,335,1,6,0,0];
private static __ATN: ATN;
public static get _ATN(): ATN {

View File

@@ -21,6 +21,8 @@ import { FunctionParamContext } from "./FilterQueryParser.js";
import { ArrayContext } from "./FilterQueryParser.js";
import { ValueContext } from "./FilterQueryParser.js";
import { KeyContext } from "./FilterQueryParser.js";
import { FieldContext } from "./FilterQueryParser.js";
import { ExactCallContext } from "./FilterQueryParser.js";
/**
@@ -208,5 +210,25 @@ export default class FilterQueryListener extends ParseTreeListener {
* @param ctx the parse tree
*/
exitKey?: (ctx: KeyContext) => void;
/**
* Enter a parse tree produced by `FilterQueryParser.field`.
* @param ctx the parse tree
*/
enterField?: (ctx: FieldContext) => void;
/**
* Exit a parse tree produced by `FilterQueryParser.field`.
* @param ctx the parse tree
*/
exitField?: (ctx: FieldContext) => void;
/**
* Enter a parse tree produced by `FilterQueryParser.exactCall`.
* @param ctx the parse tree
*/
enterExactCall?: (ctx: ExactCallContext) => void;
/**
* Exit a parse tree produced by `FilterQueryParser.exactCall`.
* @param ctx the parse tree
*/
exitExactCall?: (ctx: ExactCallContext) => void;
}

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,8 @@ import { FunctionParamContext } from "./FilterQueryParser.js";
import { ArrayContext } from "./FilterQueryParser.js";
import { ValueContext } from "./FilterQueryParser.js";
import { KeyContext } from "./FilterQueryParser.js";
import { FieldContext } from "./FilterQueryParser.js";
import { ExactCallContext } from "./FilterQueryParser.js";
/**
@@ -139,5 +141,17 @@ export default class FilterQueryVisitor<Result> extends ParseTreeVisitor<Result>
* @return the visitor result
*/
visitKey?: (ctx: KeyContext) => Result;
/**
* Visit a parse tree produced by `FilterQueryParser.field`.
* @param ctx the parse tree
* @return the visitor result
*/
visitField?: (ctx: FieldContext) => Result;
/**
* Visit a parse tree produced by `FilterQueryParser.exactCall`.
* @param ctx the parse tree
* @return the visitor result
*/
visitExactCall?: (ctx: ExactCallContext) => Result;
}

View File

@@ -246,6 +246,12 @@
padding-left: 6px !important;
}
&__label {
display: inline-flex;
align-items: baseline;
gap: 6px;
}
&__pinned-icon {
flex-shrink: 0;
color: var(--text-robin-400);

View File

@@ -76,6 +76,7 @@ export interface PrettyViewProps {
value: unknown,
keyPath: readonly (string | number)[],
) => React.ReactNode | undefined;
labelSuffixRenderer?: (fieldKey: string) => React.ReactNode;
}
function PrettyView({
@@ -88,6 +89,7 @@ function PrettyView({
pinnedFieldsValue,
onPinnedFieldsChange,
renderLeafValue,
labelSuffixRenderer,
}: PrettyViewProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [, setCopy] = useCopyToClipboard();
@@ -318,10 +320,24 @@ function PrettyView({
}}
/>
<span>{displayKey}</span>
{labelSuffixRenderer?.(displayKey)}
</span>
);
},
[togglePin, pinnedEntries],
[togglePin, pinnedEntries, labelSuffixRenderer],
);
const labelRenderer = useCallback(
(keyPath: KeyPath): React.ReactNode => {
const displayKey = String(keyPath[0]);
return (
<span className="pretty-view__label">
<span>{displayKey}</span>
{labelSuffixRenderer?.(displayKey)}
</span>
);
},
[labelSuffixRenderer],
);
return (
@@ -364,6 +380,7 @@ function PrettyView({
shouldExpandNodeInitially={shouldExpandNodeInitially}
valueRenderer={valueRenderer}
getItemString={getItemString}
labelRenderer={labelRenderer}
/>
</div>
);

View File

@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import ROUTES from 'constants/routes';
import { toast } from '@signozhq/ui/sonner';
import { screen, userEvent, waitFor } from 'storybook/test';
import type { GlobalMockArgs } from '../globals';
@@ -108,3 +109,24 @@ export const WithoutStripAddCardModal: Story = {
});
},
};
/** Raises a toast and waits for it, so the shot is taken with it on screen. */
const raiseToast: NonNullable<Story['play']> = async () => {
toast.success('Service account deleted');
await screen.findByText('Service account deleted', undefined, untilLoaded);
};
/**
* A toast with the strip on. Sonner pins itself to the foot of the viewport, so
* without an offset it lands on top of Ask Noz and Support.
*/
export const WithToast: Story = {
args: { noz: true, support: 'pylon' },
play: raiseToast,
};
/** The same toast with the flag off, back at sonner's own inset from the viewport. */
export const WithoutStripToast: Story = {
args: { bottomStrip: false, noz: true, support: 'pylon' },
play: raiseToast,
};

View File

@@ -0,0 +1,14 @@
export interface SemconvMigrationReportEntry {
current: string;
old: string;
signal: string;
services: string[];
resourceSets: number;
lastSeenUnixMilli: number;
}
export interface SemconvMigrationReport {
startUnixMilli: number;
endUnixMilli: number;
entries: SemconvMigrationReportEntry[];
}

View File

@@ -123,6 +123,8 @@ export type SpaceAggregation =
export type ColumnType = 'group' | 'aggregation';
export type FieldResolution = 'exact';
// ===================== Variable Types =====================
export type VariableType = 'query' | 'dynamic' | 'custom' | 'text';
@@ -142,6 +144,7 @@ export interface TelemetryFieldKey {
signal?: SignalType;
fieldContext?: FieldContext;
fieldDataType?: FieldDataType;
fieldResolution?: FieldResolution;
materialized?: boolean;
isIndexed?: boolean;
}

View File

@@ -0,0 +1,40 @@
import {
findOldSemconvNames,
getSemconvMembers,
getSemconvRename,
} from 'utils/semconv';
describe('semantic convention helpers', () => {
it('returns the current name for an old attribute', () => {
expect(getSemconvRename('deployment.environment')).toMatchObject({
old: 'deployment.environment',
current: 'deployment.environment.name',
});
});
it('finds old names in editor text without matching larger custom names', () => {
expect(
findOldSemconvNames(
"deployment.environment = 'prod' AND custom.db.system.value = 'x'",
),
).toStrictEqual([
expect.objectContaining({
old: 'deployment.environment',
current: 'deployment.environment.name',
}),
]);
});
it('does not warn for current names', () => {
expect(
findOldSemconvNames('deployment.environment.name = prod'),
).toStrictEqual([]);
});
it('returns current-first members for compatibility readers', () => {
expect(getSemconvMembers('http.request.method')).toStrictEqual([
'http.request.method',
'http.method',
]);
});
});

View File

@@ -0,0 +1,69 @@
import {
SEMCONV_FAMILIES,
SemconvFamily,
} from 'constants/generated/semconvFamilies.gen';
export type SemconvRename = {
old: string;
current: string;
family: SemconvFamily;
};
const OLD_NAMES = SEMCONV_FAMILIES.flatMap((family) =>
family.members.map((member) => ({
old: member.name,
current: family.current,
family,
})),
);
const OLD_NAME_INDEX = new Map(OLD_NAMES.map((rename) => [rename.old, rename]));
const FAMILY_BY_NAME = new Map(
SEMCONV_FAMILIES.flatMap((family) =>
[family.current, ...family.members.map((member) => member.name)].map(
(name) => [name, family] as const,
),
),
);
export function getSemconvRename(name: string): SemconvRename | undefined {
return OLD_NAME_INDEX.get(name);
}
/** Returns the current name first, followed by every historical spelling. */
export function getSemconvMembers(name: string): readonly string[] {
const family = FAMILY_BY_NAME.get(name);
return family
? [family.current, ...family.members.map((member) => member.name)]
: [name];
}
export function findOldSemconvNames(text: string): SemconvRename[] {
if (!text) {
return [];
}
return OLD_NAMES.filter(({ old }) => containsSemconvName(text, old));
}
function containsSemconvName(text: string, name: string): boolean {
let offset = 0;
while (offset < text.length) {
const index = text.indexOf(name, offset);
if (index === -1) {
return false;
}
const before = index === 0 ? '' : text[index - 1];
const afterIndex = index + name.length;
const after = afterIndex === text.length ? '' : text[afterIndex];
if (!isSemconvNameCharacter(before) && !isSemconvNameCharacter(after)) {
return true;
}
offset = index + 1;
}
return false;
}
function isSemconvNameCharacter(value: string): boolean {
return /[A-Za-z0-9_.-]/.test(value);
}

View File

@@ -50,30 +50,30 @@ primary
* [NOT] BETWEEN, [NOT] IN, [NOT] EXISTS, [NOT] REGEXP, [NOT] CONTAINS, etc.
*/
comparison
: key EQUALS value
| key (NOT_EQUALS | NEQ) value
| key LT value
| key LE value
| key GT value
| key GE value
: field EQUALS value
| field (NOT_EQUALS | NEQ) value
| field LT value
| field LE value
| field GT value
| field GE value
| key (LIKE | ILIKE) value
| key NOT (LIKE | ILIKE) value
| field (LIKE | ILIKE) value
| field NOT (LIKE | ILIKE) value
| key BETWEEN value AND value
| key NOT BETWEEN value AND value
| field BETWEEN value AND value
| field NOT BETWEEN value AND value
| key inClause
| key notInClause
| field inClause
| field notInClause
| key EXISTS
| key NOT EXISTS
| field EXISTS
| field NOT EXISTS
| key REGEXP value
| key NOT REGEXP value
| field REGEXP value
| field NOT REGEXP value
| key CONTAINS value
| key NOT CONTAINS value
| field CONTAINS value
| field NOT CONTAINS value
;
// in(...) or in[...]
@@ -126,7 +126,7 @@ functionParamList
;
functionParam
: key
: field
| value
| array
;
@@ -155,6 +155,17 @@ key
: KEY
;
// exact(key) disables semantic-convention family resolution for this field.
// It is deliberately a field wrapper rather than a general function.
field
: key
| exactCall
;
exactCall
: EXACT LPAREN key RPAREN
;
/*
* Lexer Rules
@@ -195,6 +206,7 @@ HAS : [Hh][Aa][Ss] ;
HASANY : [Hh][Aa][Ss][Aa][Nn][Yy] ;
HASALL : [Hh][Aa][Ss][Aa][Ll][Ll] ;
SEARCH : [Ss][Ee][Aa][Rr][Cc][Hh] ;
EXACT : [Ee][Xx][Aa][Cc][Tt] ;
// Potential boolean constants
BOOL

View File

@@ -46,5 +46,23 @@ func (provider *provider) addFieldsRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/fields/semconv-migration", handler.New(provider.authzMiddleware.ViewAccess(provider.fieldsHandler.GetSemconvMigrationReport), handler.OpenAPIDef{
ID: "GetSemconvMigrationReport",
Tags: []string{"fields"},
Summary: "Get semantic-convention migration report",
Description: "Returns services that still emit old semantic-convention names without the current family name",
Request: nil,
RequestQuery: new(telemetrytypes.PostableSemconvMigrationReportParams),
RequestContentType: "",
Response: new(telemetrytypes.GettableSemconvMigrationReport),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -51,9 +51,9 @@ func postableRuleExamples() []handler.OpenAPIExample {
"filter": map[string]any{"expression": "k8s.deployment.name = 'api-service'"},
"groupBy": []any{
map[string]any{"name": "k8s.pod.name", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment.name", "fieldContext": "resource", "fieldDataType": "string"},
},
"legend": "{{k8s.pod.name}} ({{deployment.environment}})",
"legend": "{{k8s.pod.name}} ({{deployment.environment.name}})",
},
},
},
@@ -74,12 +74,12 @@ func postableRuleExamples() []handler.OpenAPIExample {
},
"evaluation": rolling("15m", "1m"),
"notificationSettings": map[string]any{
"groupBy": []any{"k8s.pod.name", "deployment.environment"},
"groupBy": []any{"k8s.pod.name", "deployment.environment.name"},
"renotify": renotify("4h", "firing"),
},
"labels": map[string]any{"severity": "critical", "team": "platform"},
"annotations": map[string]any{
"description": "Pod {{$k8s.pod.name}} CPU is at {{$value}} of request in {{$deployment.environment}}.",
"description": "Pod {{$k8s.pod.name}} CPU is at {{$value}} of request in {{$deployment.environment.name}}.",
"summary": "Pod CPU above {{$threshold}} of request",
},
},
@@ -170,7 +170,7 @@ func postableRuleExamples() []handler.OpenAPIExample {
{
Name: "metric_promql",
Summary: "Metric threshold PromQL rule",
Description: "PromQL expression instead of the builder. Dotted OTEL resource attributes are quoted (\"deployment.environment\"). Useful for queries that combine series with group_right or other Prom operators.",
Description: "PromQL expression instead of the builder. Dotted OTEL resource attributes are quoted (\"deployment.environment.name\"). Useful for queries that combine series with group_right or other Prom operators.",
Value: map[string]any{
"alert": "Kafka consumer group lag above 1000",
"alertType": "METRIC_BASED_ALERT",
@@ -187,7 +187,7 @@ func postableRuleExamples() []handler.OpenAPIExample {
"type": "promql",
"spec": map[string]any{
"name": "A",
"query": "(max by(topic, partition, \"deployment.environment\")(kafka_log_end_offset) - on(topic, partition, \"deployment.environment\") group_right max by(group, topic, partition, \"deployment.environment\")(kafka_consumer_committed_offset)) > 0",
"query": "(max by(topic, partition, \"deployment.environment.name\")(kafka_log_end_offset) - on(topic, partition, \"deployment.environment.name\") group_right max by(group, topic, partition, \"deployment.environment.name\")(kafka_consumer_committed_offset)) > 0",
"legend": "{{topic}}/{{partition}} ({{group}})",
},
},
@@ -299,9 +299,9 @@ func postableRuleExamples() []handler.OpenAPIExample {
"filter": map[string]any{"expression": "service.name = 'payments-api' AND severity_text = 'ERROR' AND body CONTAINS 'panic'"},
"groupBy": []any{
map[string]any{"name": "k8s.pod.name", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment.name", "fieldContext": "resource", "fieldDataType": "string"},
},
"legend": "{{k8s.pod.name}} ({{deployment.environment}})",
"legend": "{{k8s.pod.name}} ({{deployment.environment.name}})",
},
},
},
@@ -322,12 +322,12 @@ func postableRuleExamples() []handler.OpenAPIExample {
},
"evaluation": rolling("5m", "1m"),
"notificationSettings": map[string]any{
"groupBy": []any{"k8s.pod.name", "deployment.environment"},
"groupBy": []any{"k8s.pod.name", "deployment.environment.name"},
"renotify": renotify("15m", "firing"),
},
"labels": map[string]any{"severity": "critical", "team": "payments"},
"annotations": map[string]any{
"description": "{{$k8s.pod.name}} emitted {{$value}} panic log(s) in {{$deployment.environment}}.",
"description": "{{$k8s.pod.name}} emitted {{$value}} panic log(s) in {{$deployment.environment.name}}.",
"summary": "Payments service panic",
},
},
@@ -358,7 +358,7 @@ func postableRuleExamples() []handler.OpenAPIExample {
"disabled": true,
"aggregations": []any{map[string]any{"expression": "count()"}},
"filter": map[string]any{"expression": "service.name = 'payments-api' AND severity_text IN ['ERROR', 'FATAL']"},
"groupBy": []any{map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"}},
"groupBy": []any{map[string]any{"name": "deployment.environment.name", "fieldContext": "resource", "fieldDataType": "string"}},
},
},
map[string]any{
@@ -370,7 +370,7 @@ func postableRuleExamples() []handler.OpenAPIExample {
"disabled": true,
"aggregations": []any{map[string]any{"expression": "count()"}},
"filter": map[string]any{"expression": "service.name = 'payments-api'"},
"groupBy": []any{map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"}},
"groupBy": []any{map[string]any{"name": "deployment.environment.name", "fieldContext": "resource", "fieldDataType": "string"}},
},
},
map[string]any{
@@ -378,7 +378,7 @@ func postableRuleExamples() []handler.OpenAPIExample {
"spec": map[string]any{
"name": "F1",
"expression": "(A / B) * 100",
"legend": "{{deployment.environment}}",
"legend": "{{deployment.environment.name}}",
},
},
},
@@ -399,12 +399,12 @@ func postableRuleExamples() []handler.OpenAPIExample {
},
"evaluation": rolling("5m", "1m"),
"notificationSettings": map[string]any{
"groupBy": []any{"deployment.environment"},
"groupBy": []any{"deployment.environment.name"},
"renotify": renotify("30m", "firing"),
},
"labels": map[string]any{"severity": "critical", "team": "payments"},
"annotations": map[string]any{
"description": "Error log rate in {{$deployment.environment}} is {{$value}}%",
"description": "Error log rate in {{$deployment.environment.name}} is {{$value}}%",
"summary": "Payments-api error rate above {{$threshold}}%",
},
},
@@ -669,10 +669,10 @@ func postableRuleExamples() []handler.OpenAPIExample {
"stepInterval": 60,
"disabled": true,
"aggregations": []any{map[string]any{"expression": "count()"}},
"filter": map[string]any{"expression": "service.name CONTAINS 'api' AND http.status_code >= 500"},
"filter": map[string]any{"expression": "service.name CONTAINS 'api' AND http.response.status_code >= 500"},
"groupBy": []any{
map[string]any{"name": "service.name", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment.name", "fieldContext": "resource", "fieldDataType": "string"},
},
},
},
@@ -687,7 +687,7 @@ func postableRuleExamples() []handler.OpenAPIExample {
"filter": map[string]any{"expression": "service.name CONTAINS 'api'"},
"groupBy": []any{
map[string]any{"name": "service.name", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"},
map[string]any{"name": "deployment.environment.name", "fieldContext": "resource", "fieldDataType": "string"},
},
},
},
@@ -696,7 +696,7 @@ func postableRuleExamples() []handler.OpenAPIExample {
"spec": map[string]any{
"name": "F1",
"expression": "(A / B) * 100",
"legend": "{{service.name}} ({{deployment.environment}})",
"legend": "{{service.name}} ({{deployment.environment.name}})",
},
},
},
@@ -717,14 +717,14 @@ func postableRuleExamples() []handler.OpenAPIExample {
},
"evaluation": rolling("5m", "1m"),
"notificationSettings": map[string]any{
"groupBy": []any{"service.name", "deployment.environment"},
"groupBy": []any{"service.name", "deployment.environment.name"},
"newGroupEvalDelay": "2m",
"usePolicy": false,
"renotify": renotify("30m", "firing", "nodata"),
},
"labels": map[string]any{"team": "platform"},
"annotations": map[string]any{
"description": "{{$service.name}} 5xx rate in {{$deployment.environment}} is {{$value}}%.",
"description": "{{$service.name}} 5xx rate in {{$deployment.environment.name}} is {{$value}}%.",
"summary": "API service error rate elevated",
},
},

View File

@@ -188,11 +188,15 @@ func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) any {
// VisitComparison visits comparison expressions.
func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) any {
if ctx.Key() == nil {
if ctx.Field() == nil {
return nil
}
key := ctx.Key().GetText()
field := ctx.Field().GetText()
key := field
if exactCall := ctx.Field().ExactCall(); exactCall != nil {
key = exactCall.Key().GetText()
}
r.keysSeen[key] = struct{}{}
// Check if this key is in the labels and was part of group by
@@ -200,13 +204,13 @@ func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) any
if _, partOfGroup := r.groupBySet[key]; partOfGroup {
// Case 1: Replace with actual value
escapedValue := escapeValueIfNeeded(value)
fmt.Fprintf(&r.rewritten, "%s=%s", key, escapedValue)
fmt.Fprintf(&r.rewritten, "%s=%s", field, escapedValue)
return nil
}
}
// Otherwise, keep the original comparison
r.rewritten.WriteString(key)
r.rewritten.WriteString(field)
if ctx.EQUALS() != nil {
r.rewritten.WriteString("=")
@@ -409,8 +413,8 @@ func (r *WhereClauseRewriter) VisitFunctionParamList(ctx *parser.FunctionParamLi
// VisitFunctionParam visits function parameters.
func (r *WhereClauseRewriter) VisitFunctionParam(ctx *parser.FunctionParamContext) any {
if ctx.Key() != nil {
ctx.Key().Accept(r)
if ctx.Field() != nil {
r.rewritten.WriteString(ctx.Field().GetText())
} else if ctx.Value() != nil {
ctx.Value().Accept(r)
} else if ctx.Array() != nil {

View File

@@ -234,6 +234,18 @@ func TestPrepareFiltersV5(t *testing.T) {
expected: "(error_details EXISTS) AND service.name='serviceA'",
description: "Should preserve EXISTS operator",
},
{
name: "exact_field_label_replacement",
labels: map[string]string{
"deployment.environment": "production",
},
whereClause: "exact(deployment.environment) = 'staging'",
groupByItems: []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "deployment.environment"}},
},
expected: "exact(deployment.environment)='production'",
description: "Should keep the exact wrapper when replacing a grouped label",
},
{
name: "empty_where_clause_with_labels",

View File

@@ -76,7 +76,7 @@ func MustNewRegistry() featuretypes.Registry {
Name: FeatureResolveSemconvFamilies,
Kind: featuretypes.KindBoolean,
Stage: featuretypes.StageExperimental,
Description: "Controls whether trace queries resolve a semantic-convention name to all the spellings of its family",
Description: "Controls whether trace, log, and metric queries resolve a semantic-convention name to all the spellings of its family",
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: featuretypes.NewBooleanVariants(),
},

View File

@@ -23,13 +23,13 @@ func TestSource(t *testing.T) {
err := json.Unmarshal(buf.Bytes(), &m)
require.NoError(t, err)
assert.Contains(t, m, "code.filepath")
assert.Contains(t, m, "code.function")
assert.Contains(t, m, "code.lineno")
assert.Contains(t, m, "code.file.path")
assert.Contains(t, m, "code.function.name")
assert.Contains(t, m, "code.line.number")
assert.Contains(t, m["code.filepath"], "source_test.go")
assert.Contains(t, m["code.function"], "TestSource")
assert.NotZero(t, m["code.lineno"])
assert.Contains(t, m["code.file.path"], "source_test.go")
assert.Contains(t, m["code.function.name"], "TestSource")
assert.NotZero(t, m["code.line.number"])
// Ensure the nested "source" key is not present.
assert.NotContains(t, m, "source")

View File

@@ -8,4 +8,7 @@ type Handler interface {
// Gets the fields values for the given field value selector
GetFieldsValues(http.ResponseWriter, *http.Request)
// Gets services that still emit only historical semantic-convention names.
GetSemconvMigrationReport(http.ResponseWriter, *http.Request)
}

View File

@@ -2,6 +2,7 @@ package implfields
import (
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/binding"
@@ -16,6 +17,43 @@ type handler struct {
telemetryMetadataStore telemetrytypes.MetadataStore
}
func (handler *handler) GetSemconvMigrationReport(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
var params telemetrytypes.PostableSemconvMigrationReportParams
if err := binding.Query.BindQuery(req.URL.Query(), &params); err != nil {
render.Error(rw, err)
return
}
now := time.Now()
if params.EndUnixMilli == 0 {
params.EndUnixMilli = now.UnixMilli()
}
if params.StartUnixMilli == 0 {
params.StartUnixMilli = now.Add(-24 * time.Hour).UnixMilli()
}
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
report, err := handler.telemetryMetadataStore.GetSemconvMigrationReport(
ctx,
valuer.MustNewUUID(claims.OrgID),
params.StartUnixMilli,
params.EndUnixMilli,
)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, report)
}
func NewHandler(settings factory.ProviderSettings, telemetryMetadataStore telemetrytypes.MetadataStore) fields.Handler {
return &handler{
telemetryMetadataStore: telemetryMetadataStore,

View File

@@ -435,6 +435,7 @@ func (m *module) buildFilterClause(ctx context.Context, orgID valuer.UUID, filte
whereClauseSelectors[idx].SelectorMatchType = telemetrytypes.FieldSelectorMatchTypeExact
}
whereClauseSelectors = querybuilder.ExpandKeySelectorsForFamilies(ctx, orgID, m.fl, whereClauseSelectors)
keys, _, err := m.telemetryMetadataStore.GetKeysMulti(ctx, orgID, whereClauseSelectors)
if err != nil {
return nil, err

View File

@@ -936,6 +936,7 @@ func (m *module) buildFilterClause(ctx context.Context, orgID valuer.UUID, filte
// whereClauseSelectors[idx].Source = query.Source
}
whereClauseSelectors = querybuilder.ExpandKeySelectorsForFamilies(ctx, orgID, m.fl, whereClauseSelectors)
keys, _, err := m.telemetryMetadataStore.GetKeysMulti(ctx, orgID, whereClauseSelectors)
if err != nil {
return nil, err

File diff suppressed because one or more lines are too long

View File

@@ -25,12 +25,13 @@ HAS=24
HASANY=25
HASALL=26
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
EXACT=28
BOOL=29
NUMBER=30
QUOTED_TEXT=31
KEY=32
WS=33
FREETEXT=34
'('=1
')'=2
'['=3

File diff suppressed because one or more lines are too long

View File

@@ -25,12 +25,13 @@ HAS=24
HASANY=25
HASALL=26
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
EXACT=28
BOOL=29
NUMBER=30
QUOTED_TEXT=31
KEY=32
WS=33
FREETEXT=34
'('=1
')'=2
'['=3

View File

@@ -1,7 +1,6 @@
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
package parser // FilterQuery
import "github.com/antlr4-go/antlr/v4"
// BaseFilterQueryListener is a complete listener for a parse tree produced by FilterQueryParser.
@@ -128,3 +127,15 @@ func (s *BaseFilterQueryListener) EnterKey(ctx *KeyContext) {}
// ExitKey is called when production key is exited.
func (s *BaseFilterQueryListener) ExitKey(ctx *KeyContext) {}
// EnterField is called when production field is entered.
func (s *BaseFilterQueryListener) EnterField(ctx *FieldContext) {}
// ExitField is called when production field is exited.
func (s *BaseFilterQueryListener) ExitField(ctx *FieldContext) {}
// EnterExactCall is called when production exactCall is entered.
func (s *BaseFilterQueryListener) EnterExactCall(ctx *ExactCallContext) {}
// ExitExactCall is called when production exactCall is exited.
func (s *BaseFilterQueryListener) ExitExactCall(ctx *ExactCallContext) {}

View File

@@ -1,7 +1,6 @@
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
package parser // FilterQuery
import "github.com/antlr4-go/antlr/v4"
type BaseFilterQueryVisitor struct {
@@ -79,3 +78,11 @@ func (v *BaseFilterQueryVisitor) VisitValue(ctx *ValueContext) interface{} {
func (v *BaseFilterQueryVisitor) VisitKey(ctx *KeyContext) interface{} {
return v.VisitChildren(ctx)
}
func (v *BaseFilterQueryVisitor) VisitField(ctx *FieldContext) interface{} {
return v.VisitChildren(ctx)
}
func (v *BaseFilterQueryVisitor) VisitExactCall(ctx *ExactCallContext) interface{} {
return v.VisitChildren(ctx)
}

Some files were not shown because too many files have changed in this diff Show More