Compare commits

..

6 Commits

Author SHA1 Message Date
amlannandy
6f0f5b51a9 chore: additional performance fix 2026-02-18 14:54:49 +07:00
Amlan Kumar Nandy
037b92feb3 Merge branch 'main' into SIG-3497 2026-02-18 12:35:11 +07:00
amlannandy
95e023dafd chore: fix CI 2026-02-18 12:24:27 +07:00
amlannandy
d20c866d2c chore: additional fixes 2026-02-18 12:09:18 +07:00
amlannandy
06f55e6eda chore: search bar replacement 2026-02-18 10:58:27 +07:00
amlannandy
f592bc084d chore: metrics explorer summary page api migration 2026-02-11 20:55:03 +07:00
56 changed files with 996 additions and 3012 deletions

View File

@@ -176,7 +176,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.112.0
image: signoz/signoz:v0.111.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.112.0
image: signoz/signoz:v0.111.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -179,7 +179,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.112.0}
image: signoz/signoz:${VERSION:-v0.111.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -111,7 +111,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.112.0}
image: signoz/signoz:${VERSION:-v0.111.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -616,11 +616,11 @@ paths:
- in: query
name: signal
schema:
$ref: '#/components/schemas/TelemetrytypesSignal'
type: string
- in: query
name: source
schema:
$ref: '#/components/schemas/TelemetrytypesSource'
type: string
- in: query
name: limit
schema:
@@ -638,11 +638,11 @@ paths:
- in: query
name: fieldContext
schema:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
type: string
- in: query
name: fieldDataType
schema:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
type: string
- in: query
name: metricName
schema:
@@ -698,11 +698,11 @@ paths:
- in: query
name: signal
schema:
$ref: '#/components/schemas/TelemetrytypesSignal'
type: string
- in: query
name: source
schema:
$ref: '#/components/schemas/TelemetrytypesSource'
type: string
- in: query
name: limit
schema:
@@ -720,11 +720,11 @@ paths:
- in: query
name: fieldContext
schema:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
type: string
- in: query
name: fieldDataType
schema:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
type: string
- in: query
name: metricName
schema:
@@ -3526,62 +3526,6 @@ paths:
summary: Rotate session
tags:
- sessions
/api/v5/query_range:
post:
deprecated: false
description: Execute a composite query over a time range. Supports builder queries
(traces, logs, metrics), formulas, trace operators, PromQL, and ClickHouse
SQL.
operationId: QueryRangeV5
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeRequest'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeResponse'
status:
type: string
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"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: Query range
tags:
- query
components:
schemas:
AuthtypesAttributeMapping:
@@ -4305,39 +4249,12 @@ components:
- temporality
- isMonotonic
type: object
MetrictypesSpaceAggregation:
enum:
- ""
- sum
- avg
- min
- max
- count
- p50
- p75
- p90
- p95
- p99
type: string
MetrictypesTemporality:
enum:
- delta
- cumulative
- unspecified
type: string
MetrictypesTimeAggregation:
enum:
- ""
- latest
- sum
- avg
- min
- max
- count
- count_distinct
- rate
- increase
type: string
MetrictypesType:
enum:
- gauge
@@ -4395,103 +4312,7 @@ components:
type:
type: string
type: object
Querybuildertypesv5AggregationBucket:
properties:
alias:
type: string
anomalyScores:
items:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
type: array
index:
type: integer
lowerBoundSeries:
items:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
type: array
meta:
properties:
unit:
type: string
type: object
predictedSeries:
items:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
type: array
series:
items:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
nullable: true
type: array
upperBoundSeries:
items:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
type: array
type: object
Querybuildertypesv5Bucket:
properties:
step:
format: double
type: number
type: object
Querybuildertypesv5ClickHouseQuery:
properties:
disabled:
type: boolean
legend:
type: string
name:
type: string
query:
type: string
type: object
Querybuildertypesv5ColumnDescriptor:
properties:
aggregationIndex:
format: int64
type: integer
columnType:
$ref: '#/components/schemas/Querybuildertypesv5ColumnType'
description:
type: string
fieldContext:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
fieldDataType:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
meta:
properties:
unit:
type: string
type: object
name:
type: string
queryName:
type: string
signal:
$ref: '#/components/schemas/TelemetrytypesSignal'
unit:
type: string
required:
- name
type: object
Querybuildertypesv5ColumnType:
enum:
- group
- aggregation
type: string
Querybuildertypesv5CompositeQuery:
description: Composite query containing one or more query envelopes. Each query
envelope specifies its type and corresponding spec.
properties:
queries:
items:
$ref: '#/components/schemas/Querybuildertypesv5QueryEnvelope'
nullable: true
type: array
type: object
Querybuildertypesv5ExecStats:
description: Execution statistics for the query, including rows scanned, bytes
scanned, and duration.
properties:
bytesScanned:
minimum: 0
@@ -4513,111 +4334,10 @@ components:
expression:
type: string
type: object
Querybuildertypesv5FormatOptions:
properties:
fillGaps:
type: boolean
formatTableResultForUI:
type: boolean
type: object
Querybuildertypesv5Function:
properties:
args:
items:
$ref: '#/components/schemas/Querybuildertypesv5FunctionArg'
type: array
name:
$ref: '#/components/schemas/Querybuildertypesv5FunctionName'
type: object
Querybuildertypesv5FunctionArg:
properties:
name:
type: string
value: {}
type: object
Querybuildertypesv5FunctionName:
enum:
- cutoffmin
- cutoffmax
- clampmin
- clampmax
- absolute
- runningdiff
- log2
- log10
- cumulativesum
- ewma3
- ewma5
- ewma7
- median3
- median5
- median7
- timeshift
- anomaly
- fillzero
type: string
Querybuildertypesv5GroupByKey:
properties:
description:
type: string
fieldContext:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
fieldDataType:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
name:
type: string
signal:
$ref: '#/components/schemas/TelemetrytypesSignal'
unit:
type: string
required:
- name
type: object
Querybuildertypesv5Having:
properties:
expression:
type: string
type: object
Querybuildertypesv5Label:
properties:
key:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
value: {}
type: object
Querybuildertypesv5LimitBy:
properties:
keys:
items:
type: string
nullable: true
type: array
value:
type: string
type: object
Querybuildertypesv5LogAggregation:
properties:
alias:
type: string
expression:
type: string
type: object
Querybuildertypesv5MetricAggregation:
properties:
metricName:
type: string
reduceTo:
$ref: '#/components/schemas/Querybuildertypesv5ReduceTo'
spaceAggregation:
$ref: '#/components/schemas/MetrictypesSpaceAggregation'
temporality:
$ref: '#/components/schemas/MetrictypesTemporality'
timeAggregation:
$ref: '#/components/schemas/MetrictypesTimeAggregation'
type: object
Querybuildertypesv5OrderBy:
properties:
direction:
$ref: '#/components/schemas/Querybuildertypesv5OrderDirection'
type: string
key:
$ref: '#/components/schemas/Querybuildertypesv5OrderByKey'
type: object
@@ -4626,406 +4346,36 @@ components:
description:
type: string
fieldContext:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
type: string
fieldDataType:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
type: string
name:
type: string
signal:
$ref: '#/components/schemas/TelemetrytypesSignal'
type: string
unit:
type: string
required:
- name
type: object
Querybuildertypesv5OrderDirection:
enum:
- asc
- desc
type: string
Querybuildertypesv5PromQuery:
properties:
disabled:
type: boolean
legend:
type: string
name:
type: string
query:
type: string
stats:
type: boolean
step:
$ref: '#/components/schemas/Querybuildertypesv5Step'
type: object
Querybuildertypesv5QueryBuilderFormula:
properties:
disabled:
type: boolean
expression:
type: string
functions:
items:
$ref: '#/components/schemas/Querybuildertypesv5Function'
type: array
having:
$ref: '#/components/schemas/Querybuildertypesv5Having'
legend:
type: string
limit:
type: integer
name:
type: string
order:
items:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
type: array
type: object
Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregation:
properties:
aggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5LogAggregation'
type: array
cursor:
type: string
disabled:
type: boolean
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
functions:
items:
$ref: '#/components/schemas/Querybuildertypesv5Function'
type: array
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
type: array
having:
$ref: '#/components/schemas/Querybuildertypesv5Having'
legend:
type: string
limit:
type: integer
limitBy:
$ref: '#/components/schemas/Querybuildertypesv5LimitBy'
name:
type: string
offset:
type: integer
order:
items:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
type: array
secondaryAggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5SecondaryAggregation'
type: array
selectFields:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
type: array
signal:
$ref: '#/components/schemas/TelemetrytypesSignal'
source:
$ref: '#/components/schemas/TelemetrytypesSource'
stepInterval:
$ref: '#/components/schemas/Querybuildertypesv5Step'
type: object
Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregation:
properties:
aggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5MetricAggregation'
type: array
cursor:
type: string
disabled:
type: boolean
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
functions:
items:
$ref: '#/components/schemas/Querybuildertypesv5Function'
type: array
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
type: array
having:
$ref: '#/components/schemas/Querybuildertypesv5Having'
legend:
type: string
limit:
type: integer
limitBy:
$ref: '#/components/schemas/Querybuildertypesv5LimitBy'
name:
type: string
offset:
type: integer
order:
items:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
type: array
secondaryAggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5SecondaryAggregation'
type: array
selectFields:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
type: array
signal:
$ref: '#/components/schemas/TelemetrytypesSignal'
source:
$ref: '#/components/schemas/TelemetrytypesSource'
stepInterval:
$ref: '#/components/schemas/Querybuildertypesv5Step'
type: object
Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation:
properties:
aggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5TraceAggregation'
type: array
cursor:
type: string
disabled:
type: boolean
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
functions:
items:
$ref: '#/components/schemas/Querybuildertypesv5Function'
type: array
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
type: array
having:
$ref: '#/components/schemas/Querybuildertypesv5Having'
legend:
type: string
limit:
type: integer
limitBy:
$ref: '#/components/schemas/Querybuildertypesv5LimitBy'
name:
type: string
offset:
type: integer
order:
items:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
type: array
secondaryAggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5SecondaryAggregation'
type: array
selectFields:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
type: array
signal:
$ref: '#/components/schemas/TelemetrytypesSignal'
source:
$ref: '#/components/schemas/TelemetrytypesSource'
stepInterval:
$ref: '#/components/schemas/Querybuildertypesv5Step'
type: object
Querybuildertypesv5QueryBuilderTraceOperator:
properties:
aggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5TraceAggregation'
type: array
cursor:
type: string
disabled:
type: boolean
expression:
type: string
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
functions:
items:
$ref: '#/components/schemas/Querybuildertypesv5Function'
type: array
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
type: array
having:
$ref: '#/components/schemas/Querybuildertypesv5Having'
legend:
type: string
limit:
type: integer
name:
type: string
offset:
type: integer
order:
items:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
type: array
returnSpansFrom:
type: string
selectFields:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
type: array
stepInterval:
$ref: '#/components/schemas/Querybuildertypesv5Step'
type: object
Querybuildertypesv5QueryData:
oneOf:
- $ref: '#/components/schemas/Querybuildertypesv5TimeSeriesData'
- $ref: '#/components/schemas/Querybuildertypesv5ScalarData'
- $ref: '#/components/schemas/Querybuildertypesv5RawData'
properties:
results:
items: {}
nullable: true
type: array
type: object
Querybuildertypesv5QueryEnvelope:
oneOf:
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilderTrace'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilderLog'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilderMetric'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeFormula'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeTraceOperator'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopePromQL'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeClickHouseSQL'
properties:
spec: {}
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
type: object
Querybuildertypesv5QueryEnvelopeBuilderLog:
properties:
spec:
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregation'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
type: object
Querybuildertypesv5QueryEnvelopeBuilderMetric:
properties:
spec:
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregation'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
type: object
Querybuildertypesv5QueryEnvelopeBuilderTrace:
properties:
spec:
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
type: object
Querybuildertypesv5QueryEnvelopeClickHouseSQL:
properties:
spec:
$ref: '#/components/schemas/Querybuildertypesv5ClickHouseQuery'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
type: object
Querybuildertypesv5QueryEnvelopeFormula:
properties:
spec:
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderFormula'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
type: object
Querybuildertypesv5QueryEnvelopePromQL:
properties:
spec:
$ref: '#/components/schemas/Querybuildertypesv5PromQuery'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
type: object
Querybuildertypesv5QueryEnvelopeTraceOperator:
properties:
spec:
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderTraceOperator'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
type: object
Querybuildertypesv5QueryRangeRequest:
description: Request body for the v5 query range endpoint. Supports builder
queries (traces, logs, metrics), formulas, joins, trace operators, PromQL,
and ClickHouse SQL queries.
example:
compositeQuery:
queries:
- spec:
aggregations:
- alias: span_count
expression: count()
filter:
expression: service.name = 'frontend'
groupBy:
- fieldContext: resource
name: service.name
limit: 10
name: A
order:
- direction: desc
key:
name: span_count
signal: traces
stepInterval: 60s
type: builder_query
end: 1.6409988e+12
requestType: time_series
schemaVersion: v1
start: 1.6409952e+12
properties:
compositeQuery:
$ref: '#/components/schemas/Querybuildertypesv5CompositeQuery'
end:
minimum: 0
type: integer
formatOptions:
$ref: '#/components/schemas/Querybuildertypesv5FormatOptions'
noCache:
type: boolean
requestType:
$ref: '#/components/schemas/Querybuildertypesv5RequestType'
schemaVersion:
type: string
start:
minimum: 0
type: integer
variables:
additionalProperties:
$ref: '#/components/schemas/Querybuildertypesv5VariableItem'
type: object
type: object
Querybuildertypesv5QueryRangeResponse:
description: 'Response from the v5 query range endpoint. The data.results array
contains typed results depending on the requestType: TimeSeriesData for time_series,
ScalarData for scalar, or RawData for raw requests.'
properties:
data:
$ref: '#/components/schemas/Querybuildertypesv5QueryData'
meta:
$ref: '#/components/schemas/Querybuildertypesv5ExecStats'
type:
$ref: '#/components/schemas/Querybuildertypesv5RequestType'
type: string
warning:
$ref: '#/components/schemas/Querybuildertypesv5QueryWarnData'
type: object
Querybuildertypesv5QueryType:
enum:
- builder_query
- builder_formula
- builder_trace_operator
- clickhouse_sql
- promql
type: string
Querybuildertypesv5QueryWarnData:
properties:
message:
@@ -5042,153 +4392,6 @@ components:
message:
type: string
type: object
Querybuildertypesv5RawData:
properties:
nextCursor:
type: string
queryName:
type: string
rows:
items:
$ref: '#/components/schemas/Querybuildertypesv5RawRow'
nullable: true
type: array
type: object
Querybuildertypesv5RawRow:
properties:
data:
additionalProperties: {}
nullable: true
type: object
timestamp:
format: date-time
type: string
type: object
Querybuildertypesv5ReduceTo:
enum:
- sum
- count
- avg
- min
- max
- last
- median
type: string
Querybuildertypesv5RequestType:
enum:
- scalar
- time_series
- raw
- raw_stream
- trace
type: string
Querybuildertypesv5ScalarData:
properties:
columns:
items:
$ref: '#/components/schemas/Querybuildertypesv5ColumnDescriptor'
nullable: true
type: array
data:
items:
items: {}
type: array
nullable: true
type: array
queryName:
type: string
type: object
Querybuildertypesv5SecondaryAggregation:
properties:
alias:
type: string
expression:
type: string
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
type: array
limit:
type: integer
limitBy:
$ref: '#/components/schemas/Querybuildertypesv5LimitBy'
order:
items:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
type: array
stepInterval:
$ref: '#/components/schemas/Querybuildertypesv5Step'
type: object
Querybuildertypesv5Step:
description: Step interval. Accepts a Go duration string (e.g., "60s", "1m",
"1h") or a number representing seconds (e.g., 60).
oneOf:
- description: Duration string (e.g., "60s", "5m", "1h").
example: 60s
type: string
- description: Duration in seconds.
example: 60
type: number
Querybuildertypesv5TimeSeries:
properties:
labels:
items:
$ref: '#/components/schemas/Querybuildertypesv5Label'
type: array
values:
items:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeriesValue'
nullable: true
type: array
type: object
Querybuildertypesv5TimeSeriesData:
properties:
aggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5AggregationBucket'
nullable: true
type: array
queryName:
type: string
type: object
Querybuildertypesv5TimeSeriesValue:
properties:
bucket:
$ref: '#/components/schemas/Querybuildertypesv5Bucket'
partial:
type: boolean
timestamp:
format: int64
type: integer
value:
format: double
type: number
values:
items:
format: double
type: number
type: array
type: object
Querybuildertypesv5TraceAggregation:
properties:
alias:
type: string
expression:
type: string
type: object
Querybuildertypesv5VariableItem:
properties:
type:
$ref: '#/components/schemas/Querybuildertypesv5VariableType'
value: {}
type: object
Querybuildertypesv5VariableType:
enum:
- query
- dynamic
- custom
- text
type: string
RenderErrorResponse:
properties:
error:
@@ -5215,23 +4418,6 @@ components:
format: date-time
type: string
type: object
TelemetrytypesFieldContext:
enum:
- metric
- log
- span
- resource
- attribute
- body
type: string
TelemetrytypesFieldDataType:
enum:
- string
- bool
- float64
- int64
- number
type: string
TelemetrytypesGettableFieldKeys:
properties:
complete:
@@ -5257,28 +4443,18 @@ components:
- values
- complete
type: object
TelemetrytypesSignal:
enum:
- traces
- logs
- metrics
type: string
TelemetrytypesSource:
enum:
- meter
type: string
TelemetrytypesTelemetryFieldKey:
properties:
description:
type: string
fieldContext:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
type: string
fieldDataType:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
type: string
name:
type: string
signal:
$ref: '#/components/schemas/TelemetrytypesSignal'
type: string
unit:
type: string
required:

View File

@@ -10,7 +10,6 @@ import (
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/http/middleware"
querierAPI "github.com/SigNoz/signoz/pkg/querier"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
@@ -107,10 +106,7 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
// v5
router.Handle("/api/v5/query_range", handler.New(
am.ViewAccess(ah.queryRangeV5),
querierAPI.QueryRangeV5OpenAPIDef,
)).Methods(http.MethodPost)
router.HandleFunc("/api/v5/query_range", am.ViewAccess(ah.queryRangeV5)).Methods(http.MethodPost)
router.HandleFunc("/api/v5/substitute_vars", am.ViewAccess(ah.QuerierAPI.ReplaceVariables)).Methods(http.MethodPost)

View File

@@ -1,9 +1,8 @@
/* eslint-disable no-nested-ternary */
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Virtuoso } from 'react-virtuoso';
import { Card } from 'antd';
import LogDetail from 'components/LogDetail';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
@@ -11,8 +10,6 @@ import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -31,15 +28,6 @@ interface Props {
}
function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
const virtuosoRef = useRef<VirtuosoHandle>(null);
const {
activeLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
const basePayload = getHostLogsQueryPayload(
timeRange.startTime,
timeRange.endTime,
@@ -84,40 +72,29 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
setIsPaginating(false);
}, [data, setIsPaginating]);
const handleScrollToLog = useScrollToLog({
logs,
virtuosoRef,
});
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => {
return (
<div key={logToRender.id}>
<RawLogView
isTextOverflowEllipsisDisabled
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
isActiveLog={activeLog?.id === logToRender.id}
/>
</div>
);
},
[activeLog, handleSetActiveLog, handleCloseLogDetail],
(_: number, logToRender: ILog): JSX.Element => (
<RawLogView
isTextOverflowEllipsisDisabled
key={logToRender.id}
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
/>
),
[],
);
const renderFooter = useCallback(
@@ -141,7 +118,6 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
<Virtuoso
className="host-metrics-logs-virtuoso"
key="host-metrics-logs-virtuoso"
ref={virtuosoRef}
data={logs}
endReached={loadMoreLogs}
totalCount={logs.length}
@@ -163,24 +139,7 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
{!isLoading && !isError && logs.length === 0 && <NoLogsContainer />}
{isError && !isLoading && <LogsError />}
{!isLoading && !isError && logs.length > 0 && (
<div
className="host-metrics-logs-list-container"
data-log-detail-ignore="true"
>
{renderContent}
</div>
)}
{selectedTab && activeLog && (
<LogDetail
log={activeLog}
onClose={handleCloseLogDetail}
logs={logs}
onNavigateLog={handleSetActiveLog}
selectedTab={selectedTab}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onScrollToLog={handleScrollToLog}
/>
<div className="host-metrics-logs-list-container">{renderContent}</div>
)}
</div>
);

View File

@@ -13,9 +13,6 @@ export type LogDetailProps = {
handleChangeSelectedView?: ChangeViewFunctionType;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
logs?: ILog[];
onNavigateLog?: (log: ILog) => void;
onScrollToLog?: (logId: string) => void;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
Pick<DrawerProps, 'onClose'>;

View File

@@ -15,8 +15,6 @@
}
.log-detail-drawer__title-right {
display: flex;
align-items: center;
.ant-btn {
display: flex;
align-items: center;
@@ -68,10 +66,6 @@
margin-bottom: 16px;
}
.log-detail-drawer__content {
height: 100%;
}
.log-detail-drawer__log {
width: 100%;
display: flex;
@@ -189,115 +183,9 @@
.ant-drawer-close {
padding: 0px;
}
.log-detail-drawer__footer-hint {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px 16px;
text-align: left;
color: var(--text-vanilla-200);
background: var(--bg-ink-400);
z-index: 10;
.log-detail-drawer__footer-hint-content {
display: flex;
align-items: center;
gap: 4px;
}
.log-detail-drawer__footer-hint-icon {
display: inline;
vertical-align: middle;
color: var(--text-vanilla-200);
}
.log-detail-drawer__footer-hint-text {
font-size: 13px;
margin: 0;
}
}
.log-arrows {
display: flex;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1);
border-radius: 6px;
padding: 2px 6px;
align-items: center;
margin-left: 8px;
}
.log-arrow-btn {
padding: 0;
min-width: 28px;
height: 28px;
border-radius: 4px;
background: var(--bg-ink-400);
color: var(--text-vanilla-400);
border: 1px solid var(--bg-ink-300);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease-in-out;
}
.log-arrow-btn-up,
.log-arrow-btn-down {
background: var(--bg-ink-400);
}
.log-arrow-btn:active,
.log-arrow-btn:focus {
background: var(--bg-ink-300);
color: var(--text-vanilla-100);
}
.log-arrow-btn[disabled] {
opacity: 0.5;
cursor: not-allowed;
background: var(--bg-ink-500);
color: var(--text-vanilla-200);
.log-arrow-btn:hover:not([disabled]) {
background: var(--bg-ink-300);
color: var(--text-vanilla-100);
}
}
}
.lightMode {
.log-arrows {
background: var(--bg-vanilla-100);
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.04);
}
.log-arrow-btn {
background: var(--bg-vanilla-100);
color: var(--text-ink-400);
border: 1px solid var(--bg-vanilla-300);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.04);
}
.log-arrow-btn-up,
.log-arrow-btn-down {
background: var(--bg-vanilla-100);
}
.log-arrow-btn:active,
.log-arrow-btn:focus {
background: var(--bg-vanilla-200);
color: var(--text-ink-500);
}
.log-arrow-btn:hover:not([disabled]) {
background: var(--bg-vanilla-200);
color: var(--text-ink-500);
}
.log-arrow-btn[disabled] {
background: var(--bg-vanilla-100);
color: var(--text-ink-200);
}
.ant-drawer-header {
border-bottom: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
@@ -364,33 +252,4 @@
color: var(--text-ink-300);
}
}
.log-detail-drawer__footer-hint {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px 16px;
text-align: left;
color: var(--text-vanilla-700);
background: var(--bg-vanilla-100);
z-index: 10;
.log-detail-drawer__footer-hint-content {
display: flex;
align-items: center;
gap: 4px;
}
.log-detail-drawer__footer-hint-icon {
display: inline;
vertical-align: middle;
color: var(--text-vanilla-700);
}
.log-detail-drawer__footer-hint-text {
font-size: 13px;
margin: 0;
}
}
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
@@ -32,12 +32,8 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
import createQueryParams from 'lib/createQueryParams';
import { cloneDeep } from 'lodash-es';
import {
ArrowDown,
ArrowUp,
BarChart2,
Braces,
ChevronDown,
ChevronUp,
Compass,
Copy,
Filter,
@@ -64,9 +60,6 @@ function LogDetailInner({
isListViewPanel = false,
listViewPanelSelectedFields,
handleChangeSelectedView,
logs,
onNavigateLog,
onScrollToLog,
}: LogDetailInnerProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query | undefined>(
@@ -81,78 +74,6 @@ function LogDetailInner({
const [isEdit, setIsEdit] = useState<boolean>(false);
const { stagedQuery, updateAllQueriesOperators } = useQueryBuilder();
// Handle clicks outside to close drawer, except on explicitly ignored regions
useEffect(() => {
const handleClickOutside = (e: MouseEvent): void => {
const target = e.target as HTMLElement;
// Don't close if clicking on explicitly ignored regions
if (target.closest('[data-log-detail-ignore="true"]')) {
return;
}
// Close the drawer for any other outside click
onClose?.(e as any);
};
document.addEventListener('mousedown', handleClickOutside);
return (): void => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [onClose]);
// Keyboard navigation - handle up/down arrow keys
// Only listen when in OVERVIEW tab
useEffect(() => {
if (
!logs ||
!onNavigateLog ||
logs.length === 0 ||
selectedView !== VIEW_TYPES.OVERVIEW
) {
return;
}
const handleKeyDown = (e: KeyboardEvent): void => {
const currentIndex = logs.findIndex((l) => l.id === log.id);
if (currentIndex === -1) {
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
e.stopPropagation();
// Navigate to previous log
if (currentIndex > 0) {
const prevLog = logs[currentIndex - 1];
onNavigateLog(prevLog);
// Trigger scroll to the log element
if (onScrollToLog) {
onScrollToLog(prevLog.id);
}
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
e.stopPropagation();
// Navigate to next log
if (currentIndex < logs.length - 1) {
const nextLog = logs[currentIndex + 1];
onNavigateLog(nextLog);
// Trigger scroll to the log element
if (onScrollToLog) {
onScrollToLog(nextLog.id);
}
}
}
};
document.addEventListener('keydown', handleKeyDown);
return (): void => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [log.id, logs, onNavigateLog, onScrollToLog, selectedView]);
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) {
return null;
@@ -306,87 +227,32 @@ function LogDetailInner({
);
const logType = log?.attributes_string?.log_level || LogType.INFO;
const currentLogIndex = logs ? logs.findIndex((l) => l.id === log.id) : -1;
const isPrevDisabled =
!logs || !onNavigateLog || logs.length === 0 || currentLogIndex <= 0;
const isNextDisabled =
!logs ||
!onNavigateLog ||
logs.length === 0 ||
currentLogIndex === logs.length - 1;
type HandleNavigateLogParams = {
direction: 'next' | 'previous';
};
const handleNavigateLog = ({ direction }: HandleNavigateLogParams): void => {
if (!logs || !onNavigateLog || currentLogIndex === -1) {
return;
}
if (direction === 'previous' && !isPrevDisabled) {
const prevLog = logs[currentLogIndex - 1];
onNavigateLog(prevLog);
onScrollToLog?.(prevLog.id);
} else if (direction === 'next' && !isNextDisabled) {
const nextLog = logs[currentLogIndex + 1];
onNavigateLog(nextLog);
onScrollToLog?.(nextLog.id);
}
};
return (
<Drawer
width="60%"
mask={false}
maskClosable={false}
maskStyle={{ background: 'none' }}
title={
<div className="log-detail-drawer__title" data-log-detail-ignore="true">
<div className="log-detail-drawer__title">
<div className="log-detail-drawer__title-left">
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
<Typography.Text className="title">Log details</Typography.Text>
</div>
<div className="log-detail-drawer__title-right">
<div className="log-arrows">
<Tooltip
title={isPrevDisabled ? '' : 'Move to previous log'}
placement="top"
mouseLeaveDelay={0}
{showOpenInExplorerBtn && (
<div className="log-detail-drawer__title-right">
<Button
className="open-in-explorer-btn"
icon={<Compass size={16} />}
onClick={handleOpenInExplorer}
>
<Button
icon={<ChevronUp size={14} />}
className="log-arrow-btn log-arrow-btn-up"
disabled={isPrevDisabled}
onClick={(): void => handleNavigateLog({ direction: 'previous' })}
/>
</Tooltip>
<Tooltip
title={isNextDisabled ? '' : 'Move to next log'}
placement="top"
mouseLeaveDelay={0}
>
<Button
icon={<ChevronDown size={14} />}
className="log-arrow-btn log-arrow-btn-down"
disabled={isNextDisabled}
onClick={(): void => handleNavigateLog({ direction: 'next' })}
/>
</Tooltip>
Open in Explorer
</Button>
</div>
{showOpenInExplorerBtn && (
<div>
<Button
className="open-in-explorer-btn"
icon={<Compass size={16} />}
onClick={handleOpenInExplorer}
>
Open in Explorer
</Button>
</div>
)}
</div>
)}
</div>
}
placement="right"
// closable
onClose={drawerCloseHandler}
open={log !== null}
style={{
@@ -397,164 +263,138 @@ function LogDetailInner({
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
<div className="log-detail-drawer__content" data-log-detail-ignore="true">
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
</Tooltip>
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
</Tooltip>
<div className="log-overflow-shadow">&nbsp;</div>
</div>
<div className="log-overflow-shadow">&nbsp;</div>
</div>
<div className="tabs-and-search">
<Radio.Group
className="views-tabs"
onChange={handleModeChange}
value={selectedView}
<div className="tabs-and-search">
<Radio.Group
className="views-tabs"
onChange={handleModeChange}
value={selectedView}
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.OVERVIEW}
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.OVERVIEW}
>
<div className="view-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.JSON ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.JSON}
>
<div className="view-title">
<Braces size={14} />
JSON
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTEXT ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTEXT}
>
<div className="view-title">
<TextSelect size={14} />
Context
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.INFRAMETRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.INFRAMETRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
</Radio.Group>
<div className="log-detail-drawer__actions">
{selectedView === VIEW_TYPES.CONTEXT && (
<Tooltip
title="Show Filters"
placement="topLeft"
aria-label="Show Filters"
>
<Button
className="action-btn"
icon={<Filter size={16} />}
onClick={handleFilterVisible}
/>
</Tooltip>
)}
<div className="view-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={selectedView === VIEW_TYPES.JSON ? 'selected_view tab' : 'tab'}
value={VIEW_TYPES.JSON}
>
<div className="view-title">
<Braces size={14} />
JSON
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTEXT ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTEXT}
>
<div className="view-title">
<TextSelect size={14} />
Context
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.INFRAMETRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.INFRAMETRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
</Radio.Group>
<div className="log-detail-drawer__actions">
{selectedView === VIEW_TYPES.CONTEXT && (
<Tooltip
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
title="Show Filters"
placement="topLeft"
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
aria-label="Show Filters"
>
<Button
className="action-btn"
icon={<Copy size={16} />}
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
icon={<Filter size={16} />}
onClick={handleFilterVisible}
/>
</Tooltip>
</div>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
<QuerySearch
onChange={(value): void => handleQueryExpressionChange(value, 0)}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}
)}
<Tooltip
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
placement="topLeft"
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
>
<Button
className="action-btn"
icon={<Copy size={16} />}
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
/>
</div>
)}
{selectedView === VIEW_TYPES.OVERVIEW && (
<Overview
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
{selectedView === VIEW_TYPES.CONTEXT && (
<ContextView
log={log}
filters={filters}
contextQuery={contextQuery}
isEdit={isEdit}
/>
)}
{selectedView === VIEW_TYPES.INFRAMETRICS && (
<InfraMetrics
clusterName={log.resources_string?.[RESOURCE_KEYS.CLUSTER_NAME] || ''}
podName={log.resources_string?.[RESOURCE_KEYS.POD_NAME] || ''}
nodeName={log.resources_string?.[RESOURCE_KEYS.NODE_NAME] || ''}
hostName={log.resources_string?.[RESOURCE_KEYS.HOST_NAME] || ''}
timestamp={log.timestamp.toString()}
dataSource={DataSource.LOGS}
/>
)}
{selectedView === VIEW_TYPES.OVERVIEW && (
<div className="log-detail-drawer__footer-hint">
<div className="log-detail-drawer__footer-hint-content">
<Typography.Text
type="secondary"
className="log-detail-drawer__footer-hint-text"
>
Use
</Typography.Text>
<ArrowUp size={14} className="log-detail-drawer__footer-hint-icon" />
<span>/</span>
<ArrowDown size={14} className="log-detail-drawer__footer-hint-icon" />
<Typography.Text
type="secondary"
className="log-detail-drawer__footer-hint-text"
>
to view previous/next log
</Typography.Text>
</div>
</div>
)}
</Tooltip>
</div>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
<QuerySearch
onChange={(value): void => handleQueryExpressionChange(value, 0)}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}
/>
</div>
)}
{selectedView === VIEW_TYPES.OVERVIEW && (
<Overview
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
{selectedView === VIEW_TYPES.CONTEXT && (
<ContextView
log={log}
filters={filters}
contextQuery={contextQuery}
isEdit={isEdit}
/>
)}
{selectedView === VIEW_TYPES.INFRAMETRICS && (
<InfraMetrics
clusterName={log.resources_string?.[RESOURCE_KEYS.CLUSTER_NAME] || ''}
podName={log.resources_string?.[RESOURCE_KEYS.POD_NAME] || ''}
nodeName={log.resources_string?.[RESOURCE_KEYS.NODE_NAME] || ''}
hostName={log.resources_string?.[RESOURCE_KEYS.HOST_NAME] || ''}
timestamp={log.timestamp.toString()}
dataSource={DataSource.LOGS}
/>
)}
</Drawer>
);
}

View File

@@ -2,11 +2,13 @@ import { memo, useCallback, useMemo } from 'react';
import { blue } from '@ant-design/colors';
import { Typography } from 'antd';
import cx from 'classnames';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
// utils
@@ -102,17 +104,12 @@ function LogSelectedField({
type ListLogViewProps = {
logData: ILog;
selectedFields: IField[];
onSetActiveLog: (
log: ILog,
selectedTab?: typeof VIEW_TYPES[keyof typeof VIEW_TYPES],
) => void;
onSetActiveLog: (log: ILog) => void;
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
activeLog?: ILog | null;
linesPerRow: number;
fontSize: FontSize;
handleChangeSelectedView?: ChangeViewFunctionType;
isActiveLog?: boolean;
onClearActiveLog?: () => void;
};
function ListLogView({
@@ -123,8 +120,7 @@ function ListLogView({
activeLog,
linesPerRow,
fontSize,
isActiveLog,
onClearActiveLog,
handleChangeSelectedView,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@@ -133,24 +129,35 @@ function ListLogView({
);
const isReadOnlyLog = !isLogsExplorerPage;
const {
activeLog: activeContextLog,
onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
} = useActiveLog();
const isDarkMode = useIsDarkMode();
const handleDetailedView = useCallback(() => {
if (isActiveLog) {
onClearActiveLog?.();
return;
}
const handlerClearActiveContextLog = useCallback(
(event: React.MouseEvent | React.KeyboardEvent) => {
event.preventDefault();
event.stopPropagation();
handleClearActiveContextLog();
},
[handleClearActiveContextLog],
);
const handleDetailedView = useCallback(() => {
onSetActiveLog(logData);
}, [logData, onSetActiveLog, isActiveLog, onClearActiveLog]);
}, [logData, onSetActiveLog]);
const handleShowContext = useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
onSetActiveLog(logData, VIEW_TYPES.CONTEXT);
handleSetActiveContextLog(logData);
},
[logData, onSetActiveLog],
[logData, handleSetActiveContextLog],
);
const updatedSelecedFields = useMemo(
@@ -179,7 +186,11 @@ function ListLogView({
return (
<>
<Container
$isActiveLog={isHighlighted || activeLog?.id === logData.id}
$isActiveLog={
isHighlighted ||
activeLog?.id === logData.id ||
activeContextLog?.id === logData.id
}
$isDarkMode={isDarkMode}
$logType={logType}
onClick={handleDetailedView}
@@ -240,6 +251,15 @@ function ListLogView({
/>
)}
</Container>
{activeContextLog && (
<LogDetail
log={activeContextLog}
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</>
);
}

View File

@@ -1,15 +1,19 @@
import {
KeyboardEvent,
memo,
MouseEvent,
MouseEventHandler,
useCallback,
useMemo,
useState,
} from 'react';
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DrawerProps, Tooltip } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
// hooks
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -35,8 +39,7 @@ function RawLogView({
selectedFields = [],
fontSize,
onLogClick,
onSetActiveLog,
onClearActiveLog,
handleChangeSelectedView,
}: RawLogViewProps): JSX.Element {
const {
isHighlighted: isUrlHighlighted,
@@ -45,6 +48,15 @@ function RawLogView({
} = useCopyLogLink(data.id);
const flattenLogData = useMemo(() => FlatLogData(data), [data]);
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<VIEWS | undefined>();
const isDarkMode = useIsDarkMode();
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
@@ -122,24 +134,34 @@ function RawLogView({
// Use custom click handler if provided, otherwise use default behavior
if (onLogClick) {
onLogClick(data, event);
return;
} else {
onSetActiveLog(data);
setSelectedTab(VIEW_TYPES.OVERVIEW);
}
if (isActiveLog) {
onClearActiveLog?.();
return;
}
onSetActiveLog?.(data);
},
[isReadOnly, onLogClick, isActiveLog, onSetActiveLog, data, onClearActiveLog],
[isReadOnly, data, onSetActiveLog, onLogClick],
);
const handleCloseLogDetail: DrawerProps['onClose'] = useCallback(
(
event: MouseEvent<Element, globalThis.MouseEvent> | KeyboardEvent<Element>,
) => {
event.preventDefault();
event.stopPropagation();
onClearActiveLog();
setSelectedTab(undefined);
},
[onClearActiveLog],
);
const handleShowContext: MouseEventHandler<HTMLElement> = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
onSetActiveLog?.(data, VIEW_TYPES.CONTEXT);
// handleSetActiveContextLog(data);
setSelectedTab(VIEW_TYPES.CONTEXT);
onSetActiveLog(data);
},
[data, onSetActiveLog],
);
@@ -159,7 +181,7 @@ function RawLogView({
$isDarkMode={isDarkMode}
$isReadOnly={isReadOnly}
$isHightlightedLog={isUrlHighlighted}
$isActiveLog={isActiveLog}
$isActiveLog={activeLog?.id === data.id || isActiveLog}
$isCustomHighlighted={isHighlighted}
$logType={logType}
fontSize={fontSize}
@@ -196,6 +218,17 @@ function RawLogView({
onLogCopy={onLogCopy}
/>
)}
{selectedTab && (
<LogDetail
selectedTab={selectedTab}
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</RawLogViewContainer>
);
}

View File

@@ -45,6 +45,9 @@ export const RawLogViewContainer = styled(Row)<{
: `margin: 2px 0;`}
}
${({ $isActiveLog, $logType }): string =>
getActiveLogBackground($isActiveLog, true, $logType)}
${({ $isReadOnly, $isActiveLog, $isDarkMode, $logType }): string =>
$isActiveLog
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)

View File

@@ -1,5 +1,4 @@
import { MouseEvent } from 'react';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
@@ -17,11 +16,6 @@ export interface RawLogViewProps {
selectedFields?: IField[];
onLogClick?: (log: ILog, event: MouseEvent) => void;
handleChangeSelectedView?: ChangeViewFunctionType;
onSetActiveLog?: (
log: ILog,
selectedTab?: typeof VIEW_TYPES[keyof typeof VIEW_TYPES],
) => void;
onClearActiveLog?: () => void;
}
export interface RawLogContentProps {

View File

@@ -86,6 +86,7 @@ interface QuerySearchProps {
signalSource?: string;
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
onRun?: (query: string) => void;
showFilterSuggestionsWithoutMetric?: boolean;
}
function QuerySearch({
@@ -96,6 +97,7 @@ function QuerySearch({
onRun,
signalSource,
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
}: QuerySearchProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
@@ -252,7 +254,8 @@ function QuerySearch({
async (searchText?: string): Promise<void> => {
if (
dataSource === DataSource.METRICS &&
!queryData.aggregateAttribute?.key
!queryData.aggregateAttribute?.key &&
!showFilterSuggestionsWithoutMetric
) {
setKeySuggestions([]);
return;
@@ -301,6 +304,7 @@ function QuerySearch({
queryData.aggregateAttribute?.key,
signalSource,
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
],
);
@@ -1562,6 +1566,7 @@ QuerySearch.defaultProps = {
hardcodedAttributeKeys: undefined,
placeholder:
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')",
showFilterSuggestionsWithoutMetric: false,
};
export default QuerySearch;

View File

@@ -1,9 +1,8 @@
/* eslint-disable no-nested-ternary */
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Virtuoso } from 'react-virtuoso';
import { Card } from 'antd';
import LogDetail from 'components/LogDetail';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
@@ -12,8 +11,6 @@ import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -43,15 +40,6 @@ function EntityLogs({
category,
queryKeyFilters,
}: Props): JSX.Element {
const virtuosoRef = useRef<VirtuosoHandle>(null);
const {
activeLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
const basePayload = getEntityEventsOrLogsQueryPayload(
timeRange.startTime,
timeRange.endTime,
@@ -74,40 +62,29 @@ function EntityLogs({
basePayload,
});
const handleScrollToLog = useScrollToLog({
logs,
virtuosoRef,
});
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => {
return (
<div key={logToRender.id}>
<RawLogView
isTextOverflowEllipsisDisabled
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
isActiveLog={activeLog?.id === logToRender.id}
/>
</div>
);
},
[activeLog, handleSetActiveLog, handleCloseLogDetail],
(_: number, logToRender: ILog): JSX.Element => (
<RawLogView
isTextOverflowEllipsisDisabled
key={logToRender.id}
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
/>
),
[],
);
const { data, isLoading, isFetching, isError } = useQuery({
@@ -154,7 +131,6 @@ function EntityLogs({
<Virtuoso
className="entity-logs-virtuoso"
key="entity-logs-virtuoso"
ref={virtuosoRef}
data={logs}
endReached={loadMoreLogs}
totalCount={logs.length}
@@ -178,21 +154,7 @@ function EntityLogs({
)}
{isError && !isLoading && <LogsError />}
{!isLoading && !isError && logs.length > 0 && (
<div className="entity-logs-list-container" data-log-detail-ignore="true">
{renderContent}
</div>
)}
{selectedTab && activeLog && (
<LogDetail
log={activeLog}
onClose={handleCloseLogDetail}
logs={logs}
onNavigateLog={handleSetActiveLog}
selectedTab={selectedTab}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onScrollToLog={handleScrollToLog}
/>
<div className="entity-logs-list-container">{renderContent}</div>
)}
</div>
);

View File

@@ -2,6 +2,7 @@ import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Card, Typography } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
@@ -13,9 +14,8 @@ import { InfinityWrapperStyled } from 'container/LogsExplorerList/styles';
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
import { useOptionsMenu } from 'container/OptionsMenu';
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { useEventSource } from 'providers/EventSource';
// interfaces
import { ILog } from 'types/api/logs/log';
@@ -38,11 +38,10 @@ function LiveLogsList({
const {
activeLog,
onClearActiveLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
onSetActiveLog,
} = useActiveLog();
// get only data from the logs object
const formattedLogs: ILog[] = useMemo(
@@ -66,56 +65,42 @@ function LiveLogsList({
...options.selectColumns,
]);
const handleScrollToLog = useScrollToLog({
logs: formattedLogs,
virtuosoRef: ref,
});
const getItemContent = useCallback(
(_: number, log: ILog): JSX.Element => {
if (options.format === 'raw') {
return (
<div key={log.id}>
<RawLogView
data={log}
isActiveLog={activeLog?.id === log.id}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
/>
</div>
<RawLogView
key={log.id}
data={log}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
}
return (
<div key={log.id}>
<ListLogView
logData={log}
isActiveLog={activeLog?.id === log.id}
selectedFields={selectedFields}
linesPerRow={options.maxLines}
onAddToQuery={onAddToQuery}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
</div>
<ListLogView
key={log.id}
logData={log}
selectedFields={selectedFields}
linesPerRow={options.maxLines}
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
},
[
handleChangeSelectedView,
onAddToQuery,
onSetActiveLog,
options.fontSize,
options.format,
options.maxLines,
options.fontSize,
activeLog?.id,
selectedFields,
onAddToQuery,
handleSetActiveLog,
handleCloseLogDetail,
handleChangeSelectedView,
],
);
@@ -171,10 +156,6 @@ function LiveLogsList({
activeLogIndex,
}}
handleChangeSelectedView={handleChangeSelectedView}
logs={formattedLogs}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
activeLog={activeLog}
/>
) : (
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
@@ -192,17 +173,14 @@ function LiveLogsList({
</InfinityWrapperStyled>
)}
{activeLog && selectedTab && (
{activeLog && (
<LogDetail
selectedTab={selectedTab}
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={handleCloseLogDetail}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
logs={formattedLogs}
onNavigateLog={handleSetActiveLog}
onScrollToLog={handleScrollToLog}
/>
)}
</div>

View File

@@ -395,7 +395,7 @@ export default function TableViewActions(
onOpenChange={setIsOpen}
arrow={false}
content={
<div data-log-detail-ignore="true">
<div>
<Button
className="more-filter-actions"
type="text"
@@ -481,7 +481,7 @@ export default function TableViewActions(
onOpenChange={setIsOpen}
arrow={false}
content={
<div data-log-detail-ignore="true">
<div>
<Button
className="more-filter-actions"
type="text"

View File

@@ -7,7 +7,6 @@ import {
useMemo,
} from 'react';
import { ColumnsType } from 'antd/es/table';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import LogLinesActionButtons from 'components/Logs/LogLinesActionButtons/LogLinesActionButtons';
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { FontSize } from 'container/OptionsMenu/types';
@@ -23,27 +22,22 @@ interface TableRowProps {
tableColumns: ColumnsType<Record<string, unknown>>;
index: number;
log: Record<string, unknown>;
onShowLogDetails?: (
log: ILog,
selectedTab?: typeof VIEW_TYPES[keyof typeof VIEW_TYPES],
) => void;
handleSetActiveContextLog: (log: ILog) => void;
onShowLogDetails: (log: ILog) => void;
logs: ILog[];
hasActions: boolean;
fontSize: FontSize;
isActiveLog?: boolean;
onClearActiveLog?: () => void;
}
export default function TableRow({
tableColumns,
index,
log,
handleSetActiveContextLog,
onShowLogDetails,
logs,
hasActions,
fontSize,
isActiveLog,
onClearActiveLog,
}: TableRowProps): JSX.Element {
const isDarkMode = useIsDarkMode();
@@ -58,31 +52,21 @@ export default function TableRow({
(event) => {
event.preventDefault();
event.stopPropagation();
if (!currentLog) {
if (!handleSetActiveContextLog || !currentLog) {
return;
}
onShowLogDetails?.(currentLog, VIEW_TYPES.CONTEXT);
handleSetActiveContextLog(currentLog);
},
[currentLog, onShowLogDetails],
[currentLog, handleSetActiveContextLog],
);
const handleShowLogDetails = useCallback(() => {
if (!currentLog) {
if (!onShowLogDetails || !currentLog) {
return;
}
// If this log is already active, close the detail drawer
if (isActiveLog && onClearActiveLog) {
onClearActiveLog();
return;
}
// Otherwise, open the detail drawer for this log
if (onShowLogDetails) {
onShowLogDetails(currentLog);
}
}, [currentLog, onShowLogDetails, isActiveLog, onClearActiveLog]);
onShowLogDetails(currentLog);
}, [currentLog, onShowLogDetails]);
const hasSingleColumn =
tableColumns.filter((column) => column.key !== 'state-indicator').length ===

View File

@@ -4,6 +4,7 @@ import {
TableVirtuoso,
TableVirtuosoHandle,
} from 'react-virtuoso';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
import { useTableView } from 'components/Logs/TableView/useTableView';
@@ -57,40 +58,26 @@ const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
function InfinityTableView(
{
isLoading,
tableViewProps,
infitiyTableProps,
onSetActiveLog,
onClearActiveLog,
activeLog,
},
{ isLoading, tableViewProps, infitiyTableProps, handleChangeSelectedView },
ref,
): JSX.Element | null {
const { activeLog: activeContextLog } = useActiveLog();
const onSetActiveLogExpand = useCallback(
(log: ILog) => {
onSetActiveLog?.(log);
},
[onSetActiveLog],
);
const onSetActiveLogContext = useCallback(
(log: ILog) => {
onSetActiveLog?.(log, VIEW_TYPES.CONTEXT);
},
[onSetActiveLog],
);
const onCloseActiveLog = useCallback(() => {
onClearActiveLog?.();
}, [onClearActiveLog]);
const {
activeLog: activeContextLog,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
onAddToQuery: handleAddToQuery,
} = useActiveLog();
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const { dataSource, columns } = useTableView({
...tableViewProps,
onClickExpand: onSetActiveLogExpand,
onOpenLogsContext: onSetActiveLogContext,
onClickExpand: onSetActiveLog,
onOpenLogsContext: handleSetActiveContextLog,
});
const { draggedColumns, onDragColumns } = useDragColumns<
@@ -111,32 +98,27 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
);
const itemContent = useCallback(
(index: number, log: Record<string, unknown>): JSX.Element => {
return (
<div key={log.id as string}>
<TableRow
tableColumns={tableColumns}
index={index}
log={log}
logs={tableViewProps.logs}
hasActions
fontSize={tableViewProps.fontSize}
onShowLogDetails={onSetActiveLog}
isActiveLog={activeLog?.id === log.id}
onClearActiveLog={onCloseActiveLog}
/>
</div>
);
},
(index: number, log: Record<string, unknown>): JSX.Element => (
<TableRow
tableColumns={tableColumns}
index={index}
log={log}
handleSetActiveContextLog={handleSetActiveContextLog}
logs={tableViewProps.logs}
hasActions
fontSize={tableViewProps.fontSize}
onShowLogDetails={onSetActiveLog}
/>
),
[
handleSetActiveContextLog,
tableColumns,
onSetActiveLog,
tableViewProps.logs,
tableViewProps.fontSize,
activeLog?.id,
onCloseActiveLog,
tableViewProps.logs,
onSetActiveLog,
],
);
const tableHeader = useCallback(
() => (
<tr>
@@ -197,6 +179,24 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
? { endReached: infitiyTableProps.onEndReached }
: {})}
/>
{activeContextLog && (
<LogDetail
log={activeContextLog}
onClose={handleClearActiveContextLog}
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
</>
);
},

View File

@@ -1,7 +1,5 @@
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { UseTableViewProps } from 'components/Logs/TableView/types';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ILog } from 'types/api/logs/log';
export type InfinityTableProps = {
isLoading?: boolean;
@@ -10,11 +8,4 @@ export type InfinityTableProps = {
onEndReached: (index: number) => void;
};
handleChangeSelectedView?: ChangeViewFunctionType;
logs?: ILog[];
onSetActiveLog?: (
log: ILog,
selectedTab?: typeof VIEW_TYPES[keyof typeof VIEW_TYPES],
) => void;
onClearActiveLog?: () => void;
activeLog?: ILog | null;
};

View File

@@ -4,6 +4,7 @@ import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
// components
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
@@ -15,9 +16,8 @@ import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { useOptionsMenu } from 'container/OptionsMenu';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import APIError from 'types/api/error';
// interfaces
@@ -55,11 +55,10 @@ function LogsExplorerList({
const {
activeLog,
onClearActiveLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
onSetActiveLog,
} = useActiveLog();
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
@@ -83,12 +82,6 @@ function LogsExplorerList({
() => convertKeysToColumnFields(options.selectColumns),
[options],
);
const handleScrollToLog = useScrollToLog({
logs,
virtuosoRef: ref,
});
useEffect(() => {
if (!isLoading && !isFetching && !isError && logs.length !== 0) {
logEvent('Logs Explorer: Data present', {
@@ -101,48 +94,40 @@ function LogsExplorerList({
(_: number, log: ILog): JSX.Element => {
if (options.format === 'raw') {
return (
<div key={log.id}>
<RawLogView
data={log}
isActiveLog={activeLog?.id === log.id}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
/>
</div>
<RawLogView
key={log.id}
data={log}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
}
return (
<div key={log.id}>
<ListLogView
logData={log}
isActiveLog={activeLog?.id === log.id}
selectedFields={selectedFields}
onAddToQuery={onAddToQuery}
onSetActiveLog={handleSetActiveLog}
activeLog={activeLog}
fontSize={options.fontSize}
linesPerRow={options.maxLines}
handleChangeSelectedView={handleChangeSelectedView}
onClearActiveLog={handleCloseLogDetail}
/>
</div>
<ListLogView
key={log.id}
logData={log}
selectedFields={selectedFields}
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
activeLog={activeLog}
fontSize={options.fontSize}
linesPerRow={options.maxLines}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
},
[
options.format,
options.fontSize,
options.maxLines,
activeLog,
selectedFields,
onAddToQuery,
handleSetActiveLog,
handleChangeSelectedView,
handleCloseLogDetail,
onAddToQuery,
onSetActiveLog,
options.fontSize,
options.format,
options.maxLines,
selectedFields,
],
);
@@ -168,10 +153,6 @@ function LogsExplorerList({
}}
infitiyTableProps={{ onEndReached }}
handleChangeSelectedView={handleChangeSelectedView}
logs={logs}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
activeLog={activeLog}
/>
);
}
@@ -218,9 +199,6 @@ function LogsExplorerList({
getItemContent,
selectedFields,
handleChangeSelectedView,
handleSetActiveLog,
handleCloseLogDetail,
activeLog,
]);
const isTraceToLogsNavigation = useMemo(() => {
@@ -300,19 +278,14 @@ function LogsExplorerList({
{renderContent}
</InfinityWrapperStyled>
{selectedTab && activeLog && (
<LogDetail
selectedTab={selectedTab}
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
logs={logs}
onNavigateLog={handleSetActiveLog}
onScrollToLog={handleScrollToLog}
/>
)}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
</>
)}
</div>

View File

@@ -466,10 +466,7 @@ function LogsExplorerViewsContainer({
</div>
)}
<div
className="logs-explorer-views-type-content"
data-log-detail-ignore="true"
>
<div className="logs-explorer-views-type-content">
{showLiveLogs && (
<LiveLogs handleChangeSelectedView={handleChangeSelectedView} />
)}

View File

@@ -8,6 +8,7 @@ import {
} from 'react';
import { UseQueryResult } from 'react-query';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { ResizeTable } from 'components/ResizeTable';
import { SOMETHING_WENT_WRONG } from 'constants/api';
@@ -15,7 +16,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import Controls from 'container/Controls';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { tableStyles } from 'container/TracesExplorer/ListView/styles';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useLogsData } from 'hooks/useLogsData';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { FlatLogData } from 'lib/logs/flatLogData';
@@ -82,24 +83,24 @@ function LogsPanelComponent({
() => logs.map((log) => FlatLogData(log) as RowData),
[logs],
);
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
} = useActiveLog();
const handleRow = useCallback(
(record: RowData): HTMLAttributes<RowData> => ({
onClick: (): void => {
const log = logs.find((item) => item.id === record.id);
if (log) {
handleSetActiveLog(log);
onSetActiveLog(log);
}
},
}),
[handleSetActiveLog, logs],
[logs, onSetActiveLog],
);
const handleRequestData = (newOffset: number): void => {
@@ -131,7 +132,7 @@ function LogsPanelComponent({
return (
<>
<div className="logs-table" data-log-detail-ignore="true">
<div className="logs-table">
<div className="resize-table">
<OverlayScrollbar>
<ResizeTable
@@ -165,19 +166,15 @@ function LogsPanelComponent({
</div>
)}
</div>
{selectedTab && activeLog && (
<LogDetail
selectedTab={selectedTab}
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
isListViewPanel
listViewPanelSelectedFields={widget?.selectedLogFields}
logs={logs}
onNavigateLog={handleSetActiveLog}
/>
)}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
isListViewPanel
listViewPanelSelectedFields={widget?.selectedLogFields}
/>
</>
);
}

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import {
Button,
Empty,
@@ -10,22 +9,24 @@ import {
Popover,
Spin,
} from 'antd';
import { Filter } from 'api/v5/v5';
import {
convertExpressionToFilters,
convertFiltersToExpression,
} from 'components/QueryBuilderV2/utils';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetMetricsListFilterValues } from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Search } from 'lucide-react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { SUMMARY_FILTERS_KEY } from './constants';
function MetricNameSearch({
queryFilters,
queryFilterExpression,
onFilterChange,
}: {
queryFilters: TagFilter;
queryFilterExpression: Filter;
onFilterChange: (value: string) => void;
}): JSX.Element {
const [searchParams, setSearchParams] = useSearchParams();
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const [searchString, setSearchString] = useState<string>('');
const [debouncedSearchString, setDebouncedSearchString] = useState<string>('');
@@ -67,9 +68,12 @@ function MetricNameSearch({
const handleSelect = useCallback(
(selectedMetricName: string): void => {
const queryFilters = convertExpressionToFilters(
queryFilterExpression?.expression,
);
const newFilters = {
items: [
...queryFilters.items,
...queryFilters,
{
id: 'metric_name',
op: 'CONTAINS',
@@ -83,13 +87,11 @@ function MetricNameSearch({
],
op: 'and',
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
});
const newFilterExpression = convertFiltersToExpression(newFilters);
onFilterChange(newFilterExpression.expression);
setIsPopoverOpen(false);
},
[queryFilters.items, setSearchParams, searchParams],
[queryFilterExpression, onFilterChange],
);
const metricNameFilterValues = useMemo(

View File

@@ -1,23 +1,19 @@
import { useCallback, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Button, Menu, Popover, Tooltip } from 'antd';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
import { Search } from 'lucide-react';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import {
METRIC_TYPE_LABEL_MAP,
METRIC_TYPE_VALUES_MAP,
SUMMARY_FILTERS_KEY,
} from './constants';
import { METRIC_TYPE_LABEL_MAP_V2 } from './constants';
function MetricTypeSearch({
queryFilters,
onFilterChange,
}: {
queryFilters: TagFilter;
onFilterChange: (expression: string) => void;
}): JSX.Element {
const [searchParams, setSearchParams] = useSearchParams();
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const menuItems = useMemo(
@@ -26,9 +22,9 @@ function MetricTypeSearch({
key: 'all',
value: 'All',
},
...Object.keys(METRIC_TYPE_LABEL_MAP).map((key) => ({
key: METRIC_TYPE_VALUES_MAP[key as MetricType],
value: METRIC_TYPE_LABEL_MAP[key as MetricType],
...Object.keys(METRIC_TYPE_LABEL_MAP_V2).map((key) => ({
key: METRIC_TYPE_LABEL_MAP_V2[key as MetrictypesTypeDTO],
value: METRIC_TYPE_LABEL_MAP_V2[key as MetrictypesTypeDTO],
})),
],
[],
@@ -36,16 +32,17 @@ function MetricTypeSearch({
const handleSelect = useCallback(
(selectedMetricType: string): void => {
let newFilters;
if (selectedMetricType !== 'all') {
const newFilters = {
newFilters = {
items: [
...queryFilters.items,
{
id: 'metric_type',
id: 'ttype',
op: '=',
key: {
id: 'metric_type',
key: 'metric_type',
id: 'type',
key: 'type',
type: 'tag',
},
value: selectedMetricType,
@@ -53,23 +50,17 @@ function MetricTypeSearch({
],
op: 'AND',
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
});
} else {
const newFilters = {
items: queryFilters.items.filter((item) => item.id !== 'metric_type'),
newFilters = {
items: queryFilters.items.filter((item) => item.id !== 'type'),
op: 'AND',
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
});
}
const newFilterExpression = convertFiltersToExpression(newFilters);
onFilterChange(newFilterExpression.expression);
setIsPopoverOpen(false);
},
[queryFilters.items, setSearchParams, searchParams],
[queryFilters.items, onFilterChange],
);
const menu = (

View File

@@ -1,27 +1,59 @@
import { Tooltip } from 'antd';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useEffect, useState } from 'react';
import { Button, Tooltip } from 'antd';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { HardHat, Info } from 'lucide-react';
import { Info, Play } from 'lucide-react';
import { DataSource } from 'types/common/queryBuilder';
import { MetricsSearchProps } from './types';
function MetricsSearch({ query, onChange }: MetricsSearchProps): JSX.Element {
const [currentExpression, setCurrentExpression] = useState<string>(
query?.filter?.expression || '',
);
useEffect(() => {
const expression = query?.filter?.expression || '';
setCurrentExpression(expression);
}, [query?.filter?.expression]);
const handleOnChange = (expression: string): void => {
setCurrentExpression(expression);
};
const handleStageAndRunQuery = (): void => onChange(currentExpression);
return (
<div className="metrics-search-container">
<div className="qb-search-container">
<div data-testid="qb-search-container" className="qb-search-container">
<Tooltip
title="Use filters to refine metrics based on attributes. Example: service_name=api - Shows all metrics associated with the API service"
placement="right"
>
<Info size={16} />
</Tooltip>
<QueryBuilderSearch
query={query}
onChange={onChange}
suffixIcon={<HardHat size={16} />}
isMetricsExplorer
<QuerySearch
onChange={handleOnChange}
dataSource={DataSource.METRICS}
queryData={{
...query,
filter: {
...query?.filter,
expression: currentExpression,
},
}}
onRun={handleOnChange}
showFilterSuggestionsWithoutMetric
/>
</div>
<Button
type="primary"
onClick={handleStageAndRunQuery}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
<div className="metrics-search-options">
<DateTimeSelectionV2
showAutoRefresh={false}

View File

@@ -24,7 +24,8 @@ function MetricsTable({
setOrderBy,
totalCount,
openMetricDetails,
queryFilters,
queryFilterExpression,
onFilterChange,
}: MetricsTableProps): JSX.Element {
const handleTableChange: TableProps<MetricsListItemRowData>['onChange'] = useCallback(
(
@@ -36,13 +37,17 @@ function MetricsTable({
): void => {
if ('field' in sorter && sorter.order) {
setOrderBy({
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
key: {
name: sorter.field as string,
},
direction: sorter.order === 'ascend' ? 'asc' : 'desc',
});
} else {
setOrderBy({
columnName: 'samples',
order: 'desc',
key: {
name: 'samples',
},
direction: 'desc',
});
}
},
@@ -51,19 +56,17 @@ function MetricsTable({
return (
<div className="metrics-table-container">
{!isError && !isLoading && (
<div className="metrics-table-title" data-testid="metrics-table-title">
<Typography.Title level={4} className="metrics-table-title">
List View
</Typography.Title>
<Tooltip
title="The table displays all metrics in the selected time range. Each row represents a unique metric, and its metric name, and metadata like description, type, unit, and samples/timeseries cardinality observed in the selected time range."
placement="right"
>
<Info size={16} />
</Tooltip>
</div>
)}
<div className="metrics-table-title" data-testid="metrics-table-title">
<Typography.Title level={4} className="metrics-table-title">
List View
</Typography.Title>
<Tooltip
title="The table displays all metrics in the selected time range. Each row represents a unique metric, and its metric name, and metadata like description, type, unit, and samples/timeseries cardinality observed in the selected time range."
placement="right"
>
<Info size={16} />
</Tooltip>
</div>
<Table
loading={{
spinning: isLoading,
@@ -75,7 +78,7 @@ function MetricsTable({
),
}}
dataSource={data}
columns={getMetricsTableColumns(queryFilters)}
columns={getMetricsTableColumns(queryFilterExpression, onFilterChange)}
locale={{
emptyText: isLoading ? null : (
<div

View File

@@ -3,6 +3,7 @@ import { useWindowSize } from 'react-use';
import { Group } from '@visx/group';
import { Treemap } from '@visx/hierarchy';
import { Empty, Select, Skeleton, Tooltip, Typography } from 'antd';
import { MetricsexplorertypesTreemapModeDTO } from 'api/generated/services/sigNoz.schemas';
import { stratify, treemapBinary } from 'd3-hierarchy';
import { Info } from 'lucide-react';
@@ -12,7 +13,7 @@ import {
TREEMAP_SQUARE_PADDING,
TREEMAP_VIEW_OPTIONS,
} from './constants';
import { MetricsTreemapProps, TreemapTile, TreemapViewType } from './types';
import { MetricsTreemapProps, TreemapTile } from './types';
import {
getTreemapTileStyle,
getTreemapTileTextStyle,
@@ -40,9 +41,9 @@ function MetricsTreemap({
const treemapData = useMemo(() => {
const extracedTreemapData =
(viewType === TreemapViewType.TIMESERIES
? data?.data?.[TreemapViewType.TIMESERIES]
: data?.data?.[TreemapViewType.SAMPLES]) || [];
(viewType === MetricsexplorertypesTreemapModeDTO.timeseries
? data?.timeseries
: data?.samples) || [];
return transformTreemapData(extracedTreemapData, viewType);
}, [data, viewType]);
@@ -54,62 +55,36 @@ function MetricsTreemap({
const xMax = treemapWidth - TREEMAP_MARGINS.LEFT - TREEMAP_MARGINS.RIGHT;
const yMax = TREEMAP_HEIGHT - TREEMAP_MARGINS.TOP - TREEMAP_MARGINS.BOTTOM;
if (isLoading) {
return (
<div data-testid="metrics-treemap-loading-state">
<Skeleton
style={{ width: treemapWidth, height: TREEMAP_HEIGHT + 55 }}
active
/>
</div>
);
}
if (
!data ||
!data.data ||
(data?.status === 'success' && !data?.data?.[viewType])
) {
return (
<Empty
description="No metrics found"
data-testid="metrics-treemap-empty-state"
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
/>
);
}
if (data?.status === 'error' || isError) {
return (
<Empty
description="Error fetching metrics. If the problem persists, please contact support."
data-testid="metrics-treemap-error-state"
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
/>
);
}
return (
<div
className="metrics-treemap-container"
data-testid="metrics-treemap-container"
>
<div className="metrics-treemap-title">
<div className="metrics-treemap-title-left">
<Typography.Title level={4}>Proportion View</Typography.Title>
<Tooltip
title="The treemap displays the proportion of samples/timeseries in the selected time range. Each tile represents a unique metric, and its size indicates the percentage of samples/timeseries it contributes to the total."
placement="right"
>
<Info size={16} />
</Tooltip>
const treemapContent = useMemo(() => {
if (isLoading) {
return (
<div data-testid="metrics-treemap-loading-state">
<Skeleton style={{ width: treemapWidth, height: TREEMAP_HEIGHT }} active />
</div>
<Select
options={TREEMAP_VIEW_OPTIONS}
value={viewType}
onChange={setHeatmapView}
);
}
if (isError) {
return (
<Empty
description="Error fetching metrics. If the problem persists, please contact support."
data-testid="metrics-treemap-error-state"
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
/>
</div>
);
}
if (!data || !data?.[viewType]?.length) {
return (
<Empty
description="No metrics found"
data-testid="metrics-treemap-empty-state"
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
/>
);
}
return (
<svg
width={treemapWidth}
height={TREEMAP_HEIGHT}
@@ -174,6 +149,42 @@ function MetricsTreemap({
)}
</Treemap>
</svg>
);
}, [
data,
isError,
isLoading,
openMetricDetails,
transformedTreemapData,
treemapWidth,
viewType,
xMax,
yMax,
]);
return (
<div
className="metrics-treemap-container"
data-testid="metrics-treemap-container"
>
<div className="metrics-treemap-title">
<div className="metrics-treemap-title-left">
<Typography.Title level={4}>Proportion View</Typography.Title>
<Tooltip
title="The treemap displays the proportion of samples/timeseries in the selected time range. Each tile represents a unique metric, and its size indicates the percentage of samples/timeseries it contributes to the total."
placement="right"
>
<Info size={16} />
</Tooltip>
</div>
<Select
options={TREEMAP_VIEW_OPTIONS}
value={viewType}
onChange={setHeatmapView}
disabled={isLoading}
/>
</div>
{treemapContent}
</div>
);
}

View File

@@ -38,6 +38,7 @@
.metrics-search-container {
display: flex;
gap: 16px;
align-items: center;
.metrics-search-options {
display: flex;

View File

@@ -4,11 +4,23 @@ import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
import { initialQueriesMap } from 'constants/queryBuilder';
import {
useGetMetricsStats,
useGetMetricsTreemap,
} from 'api/generated/services/metrics';
import {
MetricsexplorertypesStatsRequestDTO,
MetricsexplorertypesTreemapModeDTO,
MetricsexplorertypesTreemapRequestDTO,
Querybuildertypesv5OrderByDTO,
} from 'api/generated/services/sigNoz.schemas';
import {
convertExpressionToFilters,
convertFiltersToExpression,
} from 'components/QueryBuilderV2/utils';
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
import NoLogs from 'container/NoLogs/NoLogs';
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
import { useGetMetricsTreeMap } from 'hooks/metricsExplorer/useGetMetricsTreeMap';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { AppState } from 'store/reducers';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
@@ -23,32 +35,38 @@ import {
IS_INSPECT_MODAL_OPEN_KEY,
IS_METRIC_DETAILS_OPEN_KEY,
SELECTED_METRIC_NAME_KEY,
SUMMARY_FILTERS_KEY,
} from './constants';
import MetricsSearch from './MetricsSearch';
import MetricsTable from './MetricsTable';
import MetricsTreemap from './MetricsTreemap';
import { OrderByPayload, TreemapViewType } from './types';
import {
convertNanoToMilliseconds,
formatDataForMetricsTable,
getMetricsListQuery,
} from './utils';
import { convertNanoToMilliseconds, formatDataForMetricsTable } from './utils';
import './Summary.styles.scss';
const DEFAULT_ORDER_BY: OrderByPayload = {
columnName: 'samples',
order: 'desc',
const DEFAULT_ORDER_BY: Querybuildertypesv5OrderByDTO = {
key: {
name: 'samples',
},
direction: 'desc',
};
function Summary(): JSX.Element {
const { pageSize, setPageSize } = usePageSize('metricsExplorer');
const [currentPage, setCurrentPage] = useState(1);
const [orderBy, setOrderBy] = useState<OrderByPayload>(DEFAULT_ORDER_BY);
const [heatmapView, setHeatmapView] = useState<TreemapViewType>(
TreemapViewType.TIMESERIES,
const [orderBy, setOrderBy] = useState<Querybuildertypesv5OrderByDTO>(
DEFAULT_ORDER_BY,
);
const [
heatmapView,
setHeatmapView,
] = useState<MetricsexplorertypesTreemapModeDTO>(
MetricsexplorertypesTreemapModeDTO.timeseries,
);
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const query = useMemo(() => currentQuery?.builder?.queryData[0], [
currentQuery,
]);
const [searchParams, setSearchParams] = useSearchParams();
const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(
@@ -65,17 +83,6 @@ function Summary(): JSX.Element {
(state) => state.globalTime,
);
const queryFilters: TagFilter = useMemo(() => {
const encodedFilters = searchParams.get(SUMMARY_FILTERS_KEY);
if (encodedFilters) {
return JSON.parse(encodedFilters);
}
return {
items: [],
op: 'AND',
};
}, [searchParams]);
useEffect(() => {
logEvent(MetricsExplorerEvents.TabChanged, {
[MetricsExplorerEventKeys.Tab]: 'summary',
@@ -87,105 +94,112 @@ function Summary(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// This is used to avoid the filters from being serialized with the id
const queryFiltersWithoutId = useMemo(() => {
const filtersWithoutId = {
...queryFilters,
items: queryFilters.items.map(({ id: _id, ...rest }) => rest),
};
return JSON.stringify(filtersWithoutId);
}, [queryFilters]);
const queryFilterExpression = useMemo(() => {
const filters = query?.filters || { items: [], op: 'AND' };
return convertFiltersToExpression(filters);
}, [query?.filters]);
const metricsListQuery = useMemo(() => {
const baseQuery = getMetricsListQuery();
const metricsListQuery: MetricsexplorertypesStatsRequestDTO = useMemo(() => {
return {
...baseQuery,
limit: pageSize,
offset: (currentPage - 1) * pageSize,
filters: queryFilters,
start: convertNanoToMilliseconds(minTime),
end: convertNanoToMilliseconds(maxTime),
limit: pageSize,
offset: (currentPage - 1) * pageSize,
orderBy,
filter: {
expression: queryFilterExpression.expression,
},
};
}, [queryFilters, minTime, maxTime, orderBy, pageSize, currentPage]);
}, [
minTime,
maxTime,
orderBy,
pageSize,
currentPage,
queryFilterExpression.expression,
]);
const metricsTreemapQuery = useMemo(
const metricsTreemapQuery: MetricsexplorertypesTreemapRequestDTO = useMemo(
() => ({
limit: 100,
filters: queryFilters,
treemap: heatmapView,
start: convertNanoToMilliseconds(minTime),
end: convertNanoToMilliseconds(maxTime),
mode: heatmapView,
filter: {
expression: queryFilterExpression.expression,
},
}),
[queryFilters, heatmapView, minTime, maxTime],
[heatmapView, minTime, maxTime, queryFilterExpression.expression],
);
const {
data: metricsData,
isLoading: isMetricsLoading,
isFetching: isMetricsFetching,
isError: isMetricsError,
} = useGetMetricsList(metricsListQuery, {
enabled: !!metricsListQuery && !isInspectModalOpen,
queryKey: [
'metricsList',
queryFiltersWithoutId,
orderBy,
pageSize,
currentPage,
minTime,
maxTime,
],
});
mutate: getMetricsStats,
isLoading: isGetMetricsStatsLoading,
isError: isGetMetricsStatsError,
} = useGetMetricsStats();
const isListViewError = useMemo(
() => isMetricsError || !!(metricsData && metricsData.statusCode !== 200),
[isMetricsError, metricsData],
() => isGetMetricsStatsError || metricsData?.status !== 200,
[isGetMetricsStatsError, metricsData],
);
const {
data: treeMapData,
isLoading: isTreeMapLoading,
isFetching: isTreeMapFetching,
isError: isTreeMapError,
} = useGetMetricsTreeMap(metricsTreemapQuery, {
enabled: !!metricsTreemapQuery && !isInspectModalOpen,
queryKey: [
'metricsTreemap',
queryFiltersWithoutId,
heatmapView,
minTime,
maxTime,
],
});
mutate: getMetricsTreemap,
isLoading: isGetMetricsTreemapLoading,
isError: isGetMetricsTreemapError,
} = useGetMetricsTreemap();
useEffect(() => {
getMetricsStats({
data: metricsListQuery,
});
getMetricsTreemap({
data: metricsTreemapQuery,
});
}, [
metricsTreemapQuery,
metricsListQuery,
getMetricsTreemap,
getMetricsStats,
]);
const isProportionViewError = useMemo(
() => isTreeMapError || treeMapData?.statusCode !== 200,
[isTreeMapError, treeMapData],
() => isGetMetricsTreemapError || treeMapData?.status !== 200,
[isGetMetricsTreemapError, treeMapData],
);
const handleFilterChange = useCallback(
(value: TagFilter) => {
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[SUMMARY_FILTERS_KEY]: JSON.stringify(value),
(expression: string) => {
const newFilters: TagFilter = {
items: convertExpressionToFilters(expression),
op: 'AND',
};
redirectWithQueryBuilderData({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
filters: newFilters,
filter: {
expression,
},
},
],
},
});
setCurrentPage(1);
if (value.items.length > 0) {
if (expression) {
logEvent(MetricsExplorerEvents.FilterApplied, {
[MetricsExplorerEventKeys.Tab]: 'summary',
});
}
},
[setSearchParams, searchParams],
);
const searchQuery = useMemo(
() => ({
...initialQueriesMap.metrics.builder.queryData[0],
filters: queryFilters,
}),
[queryFilters],
[currentQuery, redirectWithQueryBuilderData],
);
const onPaginationChange = (page: number, pageSize: number): void => {
@@ -202,7 +216,7 @@ function Summary(): JSX.Element {
};
const formattedMetricsData = useMemo(
() => formatDataForMetricsTable(metricsData?.payload?.data?.metrics || []),
() => formatDataForMetricsTable(metricsData?.data?.data?.metrics || []),
[metricsData],
);
@@ -254,7 +268,9 @@ function Summary(): JSX.Element {
});
};
const handleSetHeatmapView = (view: TreemapViewType): void => {
const handleSetHeatmapView = (
view: MetricsexplorertypesTreemapModeDTO,
): void => {
setHeatmapView(view);
logEvent(MetricsExplorerEvents.TreemapViewChanged, {
[MetricsExplorerEventKeys.Tab]: 'summary',
@@ -262,63 +278,71 @@ function Summary(): JSX.Element {
});
};
const handleSetOrderBy = (orderBy: OrderByPayload): void => {
const handleSetOrderBy = (orderBy: Querybuildertypesv5OrderByDTO): void => {
setOrderBy(orderBy);
logEvent(MetricsExplorerEvents.OrderByApplied, {
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.ColumnName]: orderBy.columnName,
[MetricsExplorerEventKeys.Order]: orderBy.order,
[MetricsExplorerEventKeys.ColumnName]: orderBy.key?.name,
[MetricsExplorerEventKeys.Order]: orderBy.direction,
});
};
const isMetricsListDataEmpty = useMemo(
() =>
formattedMetricsData.length === 0 && !isMetricsLoading && !isMetricsFetching,
[formattedMetricsData, isMetricsLoading, isMetricsFetching],
() => formattedMetricsData.length === 0 && !isGetMetricsStatsLoading,
[formattedMetricsData, isGetMetricsStatsLoading],
);
const isMetricsTreeMapDataEmpty = useMemo(
() =>
!treeMapData?.payload?.data[heatmapView]?.length &&
!isTreeMapLoading &&
!isTreeMapFetching,
!treeMapData?.data?.data?.[heatmapView]?.length &&
!isGetMetricsTreemapLoading,
[treeMapData?.data?.data, heatmapView, isGetMetricsTreemapLoading],
);
const showFullScreenLoading = useMemo(
() =>
(isGetMetricsStatsLoading || isGetMetricsTreemapLoading) &&
formattedMetricsData.length === 0 &&
!treeMapData?.data?.data?.[heatmapView]?.length,
[
treeMapData?.payload?.data,
isGetMetricsStatsLoading,
isGetMetricsTreemapLoading,
formattedMetricsData,
treeMapData,
heatmapView,
isTreeMapLoading,
isTreeMapFetching,
],
);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-summary-tab">
<MetricsSearch query={searchQuery} onChange={handleFilterChange} />
{isMetricsLoading || isTreeMapLoading ? (
<MetricsSearch query={query} onChange={handleFilterChange} />
{showFullScreenLoading ? (
<MetricsLoading />
) : isMetricsListDataEmpty && isMetricsTreeMapDataEmpty ? (
<NoLogs dataSource={DataSource.METRICS} />
) : (
<>
<MetricsTreemap
data={treeMapData?.payload}
isLoading={isTreeMapLoading || isTreeMapFetching}
data={treeMapData?.data?.data}
isLoading={isGetMetricsTreemapLoading}
isError={isProportionViewError}
viewType={heatmapView}
openMetricDetails={openMetricDetails}
setHeatmapView={handleSetHeatmapView}
/>
<MetricsTable
isLoading={isMetricsLoading || isMetricsFetching}
isLoading={isGetMetricsStatsLoading}
isError={isListViewError}
data={formattedMetricsData}
pageSize={pageSize}
currentPage={currentPage}
onPaginationChange={onPaginationChange}
setOrderBy={handleSetOrderBy}
totalCount={metricsData?.payload?.data?.total || 0}
totalCount={metricsData?.data?.data?.total || 0}
openMetricDetails={openMetricDetails}
queryFilters={queryFilters}
queryFilterExpression={queryFilterExpression}
onFilterChange={handleFilterChange}
/>
</>
)}

View File

@@ -1,10 +1,10 @@
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import { Filter } from 'api/v5/v5';
import * as useGetMetricsListFilterValues from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
import * as useQueryBuilderOperationsHooks from 'hooks/queryBuilder/useQueryBuilderOperations';
import store from 'store';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import MetricsTable from '../MetricsTable';
import { MetricsListItemRowData } from '../types';
@@ -30,9 +30,8 @@ const mockData: MetricsListItemRowData[] = [
},
];
const mockQueryFilters: TagFilter = {
items: [],
op: 'AND',
const mockQueryFilterExpression: Filter = {
expression: '',
};
jest.mock('react-router-dom-v5-compat', () => {
@@ -82,7 +81,8 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
queryFilterExpression={mockQueryFilterExpression}
onFilterChange={jest.fn()}
/>
</Provider>
</MemoryRouter>,
@@ -106,8 +106,9 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
queryFilterExpression={mockQueryFilterExpression}
isLoading
onFilterChange={jest.fn()}
/>
</Provider>
</MemoryRouter>,
@@ -130,7 +131,8 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
queryFilterExpression={mockQueryFilterExpression}
onFilterChange={jest.fn()}
/>
</Provider>
</MemoryRouter>,
@@ -158,7 +160,8 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
queryFilterExpression={mockQueryFilterExpression}
onFilterChange={jest.fn()}
/>
</Provider>
</MemoryRouter>,
@@ -187,7 +190,8 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={mockOpenMetricDetails}
queryFilters={mockQueryFilters}
queryFilterExpression={mockQueryFilterExpression}
onFilterChange={jest.fn()}
/>
</Provider>
</MemoryRouter>,
@@ -212,7 +216,8 @@ describe('MetricsTable', () => {
setOrderBy={mockSetOrderBy}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
queryFilterExpression={mockQueryFilterExpression}
onFilterChange={jest.fn()}
/>
</Provider>
</MemoryRouter>,
@@ -222,8 +227,10 @@ describe('MetricsTable', () => {
fireEvent.click(samplesHeader);
expect(mockSetOrderBy).toHaveBeenCalledWith({
columnName: 'samples',
order: 'asc',
key: {
name: 'samples',
},
direction: 'asc',
});
});
});

View File

@@ -1,10 +1,10 @@
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { MetricsexplorertypesTreemapModeDTO } from 'api/generated/services/sigNoz.schemas';
import store from 'store';
import MetricsTreemap from '../MetricsTreemap';
import { TreemapViewType } from '../types';
jest.mock('d3-hierarchy', () => ({
stratify: jest.fn().mockReturnValue({
@@ -27,14 +27,14 @@ jest.mock('react-use', () => ({
const mockData = [
{
metric_name: 'Metric 1',
metricName: 'Metric 1',
percentage: 0.5,
total_value: 15,
totalValue: 15,
},
{
metric_name: 'Metric 2',
metricName: 'Metric 2',
percentage: 0.6,
total_value: 10,
totalValue: 10,
},
];
@@ -47,14 +47,11 @@ describe('MetricsTreemap', () => {
isLoading={false}
isError={false}
data={{
status: 'success',
data: {
timeseries: [mockData[0]],
samples: [mockData[1]],
},
timeseries: [mockData[0]],
samples: [mockData[1]],
}}
openMetricDetails={jest.fn()}
viewType={TreemapViewType.SAMPLES}
viewType={MetricsexplorertypesTreemapModeDTO.samples}
setHeatmapView={jest.fn()}
/>
</Provider>
@@ -72,14 +69,11 @@ describe('MetricsTreemap', () => {
isLoading
isError={false}
data={{
status: 'success',
data: {
timeseries: [mockData[0]],
samples: [mockData[1]],
},
timeseries: [mockData[0]],
samples: [mockData[1]],
}}
openMetricDetails={jest.fn()}
viewType={TreemapViewType.SAMPLES}
viewType={MetricsexplorertypesTreemapModeDTO.samples}
setHeatmapView={jest.fn()}
/>
</Provider>
@@ -99,14 +93,11 @@ describe('MetricsTreemap', () => {
isLoading={false}
isError
data={{
status: 'success',
data: {
timeseries: [mockData[0]],
samples: [mockData[1]],
},
timeseries: [mockData[0]],
samples: [mockData[1]],
}}
openMetricDetails={jest.fn()}
viewType={TreemapViewType.SAMPLES}
viewType={MetricsexplorertypesTreemapModeDTO.samples}
setHeatmapView={jest.fn()}
/>
</Provider>
@@ -130,7 +121,7 @@ describe('MetricsTreemap', () => {
isError={false}
data={null}
openMetricDetails={jest.fn()}
viewType={TreemapViewType.SAMPLES}
viewType={MetricsexplorertypesTreemapModeDTO.samples}
setHeatmapView={jest.fn()}
/>
</Provider>

View File

@@ -1,8 +1,9 @@
import { Color } from '@signozhq/design-tokens';
import { render } from '@testing-library/react';
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { Filter } from 'api/v5/v5';
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { TreemapViewType } from '../types';
import {
@@ -11,52 +12,76 @@ import {
MetricTypeRenderer,
} from '../utils';
describe('metricsTableColumns', () => {
const mockQueryFilters: TagFilter = {
items: [],
op: 'AND',
};
const mockQueryExpression: Filter = {
expression: '',
};
const mockOnChange = jest.fn();
describe('metricsTableColumns', () => {
it('should have correct column definitions', () => {
expect(getMetricsTableColumns(mockQueryFilters)).toHaveLength(6);
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange),
).toHaveLength(6);
// Metric Name column
expect(getMetricsTableColumns(mockQueryFilters)[0].dataIndex).toBe(
'metric_name',
);
expect(getMetricsTableColumns(mockQueryFilters)[0].width).toBe(400);
expect(getMetricsTableColumns(mockQueryFilters)[0].sorter).toBe(false);
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[0].dataIndex,
).toBe('metric_name');
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[0].width,
).toBe(400);
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[0].sorter,
).toBe(false);
// Description column
expect(getMetricsTableColumns(mockQueryFilters)[1].dataIndex).toBe(
'description',
);
expect(getMetricsTableColumns(mockQueryFilters)[1].width).toBe(400);
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[1].dataIndex,
).toBe('description');
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[1].width,
).toBe(400);
// Type column
expect(getMetricsTableColumns(mockQueryFilters)[2].dataIndex).toBe(
'metric_type',
);
expect(getMetricsTableColumns(mockQueryFilters)[2].width).toBe(150);
expect(getMetricsTableColumns(mockQueryFilters)[2].sorter).toBe(false);
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[2].dataIndex,
).toBe('metric_type');
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[2].width,
).toBe(150);
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[2].sorter,
).toBe(false);
// Unit column
expect(getMetricsTableColumns(mockQueryFilters)[3].dataIndex).toBe('unit');
expect(getMetricsTableColumns(mockQueryFilters)[3].width).toBe(150);
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[3].dataIndex,
).toBe('unit');
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[3].width,
).toBe(150);
// Samples column
expect(getMetricsTableColumns(mockQueryFilters)[4].dataIndex).toBe(
TreemapViewType.SAMPLES,
);
expect(getMetricsTableColumns(mockQueryFilters)[4].width).toBe(150);
expect(getMetricsTableColumns(mockQueryFilters)[4].sorter).toBe(true);
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[4].dataIndex,
).toBe(TreemapViewType.SAMPLES);
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[4].width,
).toBe(150);
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[4].sorter,
).toBe(true);
// Time Series column
expect(getMetricsTableColumns(mockQueryFilters)[5].dataIndex).toBe(
TreemapViewType.TIMESERIES,
);
expect(getMetricsTableColumns(mockQueryFilters)[5].width).toBe(150);
expect(getMetricsTableColumns(mockQueryFilters)[5].sorter).toBe(true);
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[5].dataIndex,
).toBe(TreemapViewType.TIMESERIES);
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[5].width,
).toBe(150);
expect(
getMetricsTableColumns(mockQueryExpression, mockOnChange)[5].sorter,
).toBe(true);
});
describe('MetricTypeRenderer', () => {
@@ -111,12 +136,12 @@ describe('formatDataForMetricsTable', () => {
it('should format metrics data correctly', () => {
const mockData = [
{
metric_name: 'test_metric',
metricName: 'test_metric',
description: 'Test description',
type: MetricType.GAUGE,
type: MetrictypesTypeDTO.gauge,
unit: 'bytes',
[TreemapViewType.SAMPLES]: 1000,
[TreemapViewType.TIMESERIES]: 2000,
samples: 1000,
timeseries: 2000,
lastReceived: '2023-01-01T00:00:00Z',
},
];
@@ -163,12 +188,12 @@ describe('formatDataForMetricsTable', () => {
it('should handle empty/null values', () => {
const mockData = [
{
metric_name: '',
metricName: '',
description: '',
type: MetricType.GAUGE,
type: MetrictypesTypeDTO.gauge,
unit: '',
[TreemapViewType.SAMPLES]: 0,
[TreemapViewType.TIMESERIES]: 0,
samples: 0,
timeseries: 0,
lastReceived: '2023-01-01T00:00:00Z',
},
];

View File

@@ -1,15 +1,17 @@
import {
MetricsexplorertypesTreemapModeDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { TreemapViewType } from './types';
export const METRICS_TABLE_PAGE_SIZE = 10;
export const TREEMAP_VIEW_OPTIONS: {
value: TreemapViewType;
value: MetricsexplorertypesTreemapModeDTO;
label: string;
}[] = [
{ value: TreemapViewType.TIMESERIES, label: 'Time Series' },
{ value: TreemapViewType.SAMPLES, label: 'Samples' },
{ value: MetricsexplorertypesTreemapModeDTO.timeseries, label: 'Time Series' },
{ value: MetricsexplorertypesTreemapModeDTO.samples, label: 'Samples' },
];
export const TREEMAP_HEIGHT = 200;
@@ -17,6 +19,7 @@ export const TREEMAP_SQUARE_PADDING = 5;
export const TREEMAP_MARGINS = { TOP: 10, LEFT: 10, RIGHT: 10, BOTTOM: 10 };
// TODO: Remove this once API migration is complete
export const METRIC_TYPE_LABEL_MAP = {
[MetricType.SUM]: 'Sum',
[MetricType.GAUGE]: 'Gauge',
@@ -25,6 +28,14 @@ export const METRIC_TYPE_LABEL_MAP = {
[MetricType.EXPONENTIAL_HISTOGRAM]: 'Exp. Histogram',
};
export const METRIC_TYPE_LABEL_MAP_V2 = {
[MetrictypesTypeDTO.sum]: 'Sum',
[MetrictypesTypeDTO.gauge]: 'Gauge',
[MetrictypesTypeDTO.histogram]: 'Histogram',
[MetrictypesTypeDTO.summary]: 'Summary',
[MetrictypesTypeDTO.exponentialhistogram]: 'Exp. Histogram',
};
export const METRIC_TYPE_VALUES_MAP = {
[MetricType.SUM]: 'Sum',
[MetricType.GAUGE]: 'Gauge',
@@ -36,4 +47,3 @@ export const METRIC_TYPE_VALUES_MAP = {
export const IS_METRIC_DETAILS_OPEN_KEY = 'isMetricDetailsOpen';
export const IS_INSPECT_MODAL_OPEN_KEY = 'isInspectModalOpen';
export const SELECTED_METRIC_NAME_KEY = 'selectedMetricName';
export const SUMMARY_FILTERS_KEY = 'summaryFilters';

View File

@@ -1,9 +1,11 @@
import React from 'react';
import { MetricsTreeMapResponse } from 'api/metricsExplorer/getMetricsTreeMap';
import {
IBuilderQuery,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
MetricsexplorertypesTreemapModeDTO,
MetricsexplorertypesTreemapResponseDTO,
Querybuildertypesv5OrderByDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Filter } from 'api/v5/v5';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export interface MetricsTableProps {
isLoading: boolean;
@@ -12,24 +14,25 @@ export interface MetricsTableProps {
pageSize: number;
currentPage: number;
onPaginationChange: (page: number, pageSize: number) => void;
setOrderBy: (orderBy: OrderByPayload) => void;
setOrderBy: (orderBy: Querybuildertypesv5OrderByDTO) => void;
totalCount: number;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
queryFilters: TagFilter;
queryFilterExpression: Filter;
onFilterChange: (expression: string) => void;
}
export interface MetricsSearchProps {
query: IBuilderQuery;
onChange: (value: TagFilter) => void;
onChange: (expression: string) => void;
}
export interface MetricsTreemapProps {
data: MetricsTreeMapResponse | null | undefined;
data: MetricsexplorertypesTreemapResponseDTO | null | undefined;
isLoading: boolean;
isError: boolean;
viewType: TreemapViewType;
viewType: MetricsexplorertypesTreemapModeDTO;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
setHeatmapView: (value: TreemapViewType) => void;
setHeatmapView: (value: MetricsexplorertypesTreemapModeDTO) => void;
}
export interface OrderByPayload {

View File

@@ -3,14 +3,16 @@ import { Color } from '@signozhq/design-tokens';
import { Tooltip, Typography } from 'antd';
import { ColumnType } from 'antd/es/table';
import {
MetricsListItemData,
MetricsexplorertypesStatDTO,
MetricsexplorertypesTreemapEntryDTO,
MetricsexplorertypesTreemapModeDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import {
MetricsListPayload,
MetricType,
} from 'api/metricsExplorer/getMetricsList';
import {
SamplesData,
TimeseriesData,
} from 'api/metricsExplorer/getMetricsTreeMap';
import { Filter } from 'api/v5/v5';
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
import {
BarChart,
@@ -19,21 +21,23 @@ import {
Diff,
Gauge,
} from 'lucide-react';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { METRIC_TYPE_LABEL_MAP } from './constants';
import { METRIC_TYPE_LABEL_MAP, METRIC_TYPE_LABEL_MAP_V2 } from './constants';
import MetricNameSearch from './MetricNameSearch';
import MetricTypeSearch from './MetricTypeSearch';
import { MetricsListItemRowData, TreemapTile, TreemapViewType } from './types';
import { MetricsListItemRowData, TreemapTile } from './types';
export const getMetricsTableColumns = (
queryFilters: TagFilter,
queryFilterExpression: Filter,
onFilterChange: (expression: string) => void,
): ColumnType<MetricsListItemRowData>[] => [
{
title: (
<div className="metric-name-column-header">
<span className="metric-name-column-header-text">METRIC</span>
<MetricNameSearch queryFilters={queryFilters} />
<MetricNameSearch
queryFilterExpression={queryFilterExpression}
onFilterChange={onFilterChange}
/>
</div>
),
dataIndex: 'metric_name',
@@ -55,7 +59,11 @@ export const getMetricsTableColumns = (
title: (
<div className="metric-type-column-header">
<span className="metric-type-column-header-text">TYPE</span>
<MetricTypeSearch queryFilters={queryFilters} />
{/* TODO: @amlannandy: Re-enable once API supports metric type filtering */}
{/* <MetricTypeSearch
queryFilters={queryFilters}
onFilterChange={onFilterChange}
/> */}
</div>
),
dataIndex: 'metric_type',
@@ -69,13 +77,13 @@ export const getMetricsTableColumns = (
},
{
title: 'SAMPLES',
dataIndex: TreemapViewType.SAMPLES,
dataIndex: MetricsexplorertypesTreemapModeDTO.samples,
width: 150,
sorter: true,
},
{
title: 'TIME SERIES',
dataIndex: TreemapViewType.TIMESERIES,
dataIndex: MetricsexplorertypesTreemapModeDTO.timeseries,
width: 150,
sorter: true,
},
@@ -143,6 +151,60 @@ export function MetricTypeRenderer({
);
}
export function MetricTypeRendererV2({
type,
}: {
type: MetrictypesTypeDTO;
}): JSX.Element {
const [icon, color] = useMemo(() => {
switch (type) {
case MetrictypesTypeDTO.sum:
return [
<Diff key={type} size={12} color={Color.BG_ROBIN_500} />,
Color.BG_ROBIN_500,
];
case MetrictypesTypeDTO.gauge:
return [
<Gauge key={type} size={12} color={Color.BG_SAKURA_500} />,
Color.BG_SAKURA_500,
];
case MetrictypesTypeDTO.histogram:
return [
<BarChart2 key={type} size={12} color={Color.BG_SIENNA_500} />,
Color.BG_SIENNA_500,
];
case MetrictypesTypeDTO.summary:
return [
<BarChartHorizontal key={type} size={12} color={Color.BG_FOREST_500} />,
Color.BG_FOREST_500,
];
case MetrictypesTypeDTO.exponentialhistogram:
return [
<BarChart key={type} size={12} color={Color.BG_AQUA_500} />,
Color.BG_AQUA_500,
];
default:
return [null, ''];
}
}, [type]);
return (
<div
className="metric-type-renderer"
style={{
backgroundColor: `${color}33`,
border: `1px solid ${color}`,
color,
}}
>
{icon}
<Typography.Text style={{ color, fontSize: 12 }}>
{METRIC_TYPE_LABEL_MAP_V2[type]}
</Typography.Text>
</div>
);
}
function ValidateRowValueWrapper({
value,
children,
@@ -182,13 +244,13 @@ export const formatNumberIntoHumanReadableFormat = (
};
export const formatDataForMetricsTable = (
data: MetricsListItemData[],
data: MetricsexplorertypesStatDTO[],
): MetricsListItemRowData[] =>
data.map((metric) => ({
key: metric.metric_name,
key: metric.metricName,
metric_name: (
<ValidateRowValueWrapper value={metric.metric_name}>
<Tooltip title={metric.metric_name}>{metric.metric_name}</Tooltip>
<ValidateRowValueWrapper value={metric.metricName}>
<Tooltip title={metric.metricName}>{metric.metricName}</Tooltip>
</ValidateRowValueWrapper>
),
description: (
@@ -198,39 +260,54 @@ export const formatDataForMetricsTable = (
</Tooltip>
</ValidateRowValueWrapper>
),
metric_type: <MetricTypeRenderer type={metric.type} />,
metric_type: <MetricTypeRendererV2 type={metric.type} />,
unit: (
<ValidateRowValueWrapper value={getUniversalNameFromMetricUnit(metric.unit)}>
{getUniversalNameFromMetricUnit(metric.unit)}
</ValidateRowValueWrapper>
),
[TreemapViewType.SAMPLES]: (
<ValidateRowValueWrapper value={metric[TreemapViewType.SAMPLES]}>
<Tooltip title={metric[TreemapViewType.SAMPLES].toLocaleString()}>
{formatNumberIntoHumanReadableFormat(metric[TreemapViewType.SAMPLES])}
[MetricsexplorertypesTreemapModeDTO.samples]: (
<ValidateRowValueWrapper
value={metric[MetricsexplorertypesTreemapModeDTO.samples]}
>
<Tooltip
title={metric[MetricsexplorertypesTreemapModeDTO.samples].toLocaleString()}
>
{formatNumberIntoHumanReadableFormat(
metric[MetricsexplorertypesTreemapModeDTO.samples],
)}
</Tooltip>
</ValidateRowValueWrapper>
),
[TreemapViewType.TIMESERIES]: (
<ValidateRowValueWrapper value={metric[TreemapViewType.TIMESERIES]}>
<Tooltip title={metric[TreemapViewType.TIMESERIES].toLocaleString()}>
{formatNumberIntoHumanReadableFormat(metric[TreemapViewType.TIMESERIES])}
[MetricsexplorertypesTreemapModeDTO.timeseries]: (
<ValidateRowValueWrapper
value={metric[MetricsexplorertypesTreemapModeDTO.timeseries]}
>
<Tooltip
title={metric[
MetricsexplorertypesTreemapModeDTO.timeseries
].toLocaleString()}
>
{formatNumberIntoHumanReadableFormat(
metric[MetricsexplorertypesTreemapModeDTO.timeseries],
)}
</Tooltip>
</ValidateRowValueWrapper>
),
}));
export const transformTreemapData = (
data: TimeseriesData[] | SamplesData[],
viewType: TreemapViewType,
data: MetricsexplorertypesTreemapEntryDTO[],
viewType: MetricsexplorertypesTreemapModeDTO,
): TreemapTile[] => {
const totalSize = (data as (TimeseriesData | SamplesData)[]).reduce(
(acc: number, item: TimeseriesData | SamplesData) => acc + item.percentage,
const totalSize = data.reduce(
(acc: number, item: MetricsexplorertypesTreemapEntryDTO) =>
acc + item.percentage,
0,
);
const children = data.map((item) => ({
id: item.metric_name,
id: item.metricName,
size: totalSize > 0 ? Number((item.percentage / totalSize).toFixed(2)) : 0,
displayValue: Number(item.percentage).toFixed(2),
parent: viewType,

View File

@@ -1,59 +0,0 @@
import { useCallback, useState } from 'react';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import type { UseActiveLog } from 'hooks/logs/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { ILog } from 'types/api/logs/log';
type SelectedTab = typeof VIEW_TYPES[keyof typeof VIEW_TYPES] | undefined;
type UseLogDetailHandlersParams = {
defaultTab?: SelectedTab;
};
type UseLogDetailHandlersResult = {
activeLog: UseActiveLog['activeLog'];
onAddToQuery: UseActiveLog['onAddToQuery'];
selectedTab: SelectedTab;
handleSetActiveLog: (log: ILog, selectedTab?: SelectedTab) => void;
handleCloseLogDetail: () => void;
};
function useLogDetailHandlers({
defaultTab = VIEW_TYPES.OVERVIEW,
}: UseLogDetailHandlersParams = {}): UseLogDetailHandlersResult {
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<SelectedTab>(defaultTab);
const handleSetActiveLog = useCallback(
(log: ILog, nextTab: SelectedTab = defaultTab): void => {
if (activeLog?.id === log.id) {
onClearActiveLog();
setSelectedTab(undefined);
return;
}
onSetActiveLog(log);
setSelectedTab(nextTab ?? defaultTab);
},
[activeLog?.id, defaultTab, onClearActiveLog, onSetActiveLog],
);
const handleCloseLogDetail = useCallback((): void => {
onClearActiveLog();
setSelectedTab(undefined);
}, [onClearActiveLog]);
return {
activeLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
};
}
export default useLogDetailHandlers;

View File

@@ -1,28 +0,0 @@
import { useCallback } from 'react';
import type { VirtuosoHandle } from 'react-virtuoso';
type UseScrollToLogParams = {
logs: Array<{ id: string }>;
virtuosoRef: React.RefObject<VirtuosoHandle | null>;
};
function useScrollToLog({
logs,
virtuosoRef,
}: UseScrollToLogParams): (logId: string) => void {
return useCallback(
(logId: string): void => {
const logIndex = logs.findIndex(({ id }) => id === logId);
if (logIndex !== -1 && virtuosoRef.current) {
virtuosoRef.current.scrollToIndex({
index: logIndex,
align: 'center',
behavior: 'smooth',
});
}
},
[logs, virtuosoRef],
);
}
export default useScrollToLog;

View File

@@ -567,15 +567,6 @@ body {
border: 1px solid var(--bg-vanilla-300);
}
.ant-tooltip {
--antd-arrow-background-color: var(--bg-vanilla-100);
.ant-tooltip-inner {
background-color: var(--bg-vanilla-100);
color: var(---bg-ink-500);
}
}
.ant-dropdown-menu {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);

View File

@@ -9,9 +9,10 @@ export const getDefaultLogBackground = (
if (isReadOnly) {
return '';
}
// TODO handle the light mode here
return `&:hover {
background-color: ${
isDarkMode ? 'rgba(171, 189, 255, 0.04)' : 'rgba(0, 0, 0, 0.04)'
isDarkMode ? 'rgba(171, 189, 255, 0.04)' : 'var(--bg-vanilla-200)'
};
}`;
};
@@ -27,38 +28,22 @@ export const getActiveLogBackground = (
if (isDarkMode) {
switch (logType) {
case LogType.INFO:
return `background-color: ${Color.BG_ROBIN_500}40 !important;`;
return `background-color: ${Color.BG_ROBIN_500}10 !important;`;
case LogType.WARN:
return `background-color: ${Color.BG_AMBER_500}40 !important;`;
return `background-color: ${Color.BG_AMBER_500}10 !important;`;
case LogType.ERROR:
return `background-color: ${Color.BG_CHERRY_500}40 !important;`;
return `background-color: ${Color.BG_CHERRY_500}10 !important;`;
case LogType.TRACE:
return `background-color: ${Color.BG_FOREST_400}40 !important;`;
return `background-color: ${Color.BG_FOREST_400}10 !important;`;
case LogType.DEBUG:
return `background-color: ${Color.BG_AQUA_500}40 !important;`;
return `background-color: ${Color.BG_AQUA_500}10 !important;`;
case LogType.FATAL:
return `background-color: ${Color.BG_SAKURA_500}40 !important;`;
return `background-color: ${Color.BG_SAKURA_500}10 !important;`;
default:
return `background-color: ${Color.BG_ROBIN_500}40 !important;`;
return `background-color: ${Color.BG_SLATE_200} !important;`;
}
}
// Light mode - use lighter background colors
switch (logType) {
case LogType.INFO:
return `background-color: ${Color.BG_ROBIN_100} !important;`;
case LogType.WARN:
return `background-color: ${Color.BG_AMBER_100} !important;`;
case LogType.ERROR:
return `background-color: ${Color.BG_CHERRY_100} !important;`;
case LogType.TRACE:
return `background-color: ${Color.BG_FOREST_200} !important;`;
case LogType.DEBUG:
return `background-color: ${Color.BG_AQUA_100} !important;`;
case LogType.FATAL:
return `background-color: ${Color.BG_SAKURA_100} !important;`;
default:
return `background-color: ${Color.BG_VANILLA_300} !important;`;
}
return `background-color: ${Color.BG_VANILLA_400}!important; color: ${Color.TEXT_SLATE_400} !important;`;
};
export const getHightLightedLogBackground = (

View File

@@ -12,7 +12,6 @@ import (
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
@@ -22,24 +21,6 @@ import (
"github.com/SigNoz/signoz/pkg/variables"
)
// QueryRangeV5OpenAPIDef is the OpenAPI definition for the /api/v5/query_range endpoint.
var QueryRangeV5OpenAPIDef = handler.OpenAPIDef{
ID: "QueryRangeV5",
Tags: []string{"query"},
Summary: "Query range",
Description: "Execute a composite query over a time range. Supports builder queries (traces, logs, metrics), formulas, trace operators, PromQL, and ClickHouse SQL.",
Request: new(qbtypes.QueryRangeRequest),
RequestContentType: "application/json",
Response: new(qbtypes.QueryRangeResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: []handler.OpenAPISecurityScheme{
{Name: ctxtypes.AuthTypeAPIKey.StringValue(), Scopes: []string{"VIEWER"}},
{Name: ctxtypes.AuthTypeTokenizer.StringValue(), Scopes: []string{"VIEWER"}},
},
}
type API struct {
set factory.ProviderSettings
analytics analytics.Analytics

View File

@@ -2,7 +2,6 @@ package signoz
import (
"context"
"net/http"
"os"
"reflect"
@@ -23,9 +22,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/gorilla/mux"
"github.com/swaggest/jsonschema-go"
"github.com/swaggest/openapi-go"
"github.com/swaggest/openapi-go/openapi3"
@@ -60,10 +57,6 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
return nil, err
}
// Register routes that live outside the APIServer modules
// so they are discovered by the OpenAPI walker.
registerQueryRoutes(apiserver.Router())
reflector := openapi3.NewReflector()
reflector.JSONSchemaReflector().DefaultOptions = append(reflector.JSONSchemaReflector().DefaultOptions, jsonschema.InterceptDefName(func(t reflect.Type, defaultDefName string) string {
if defaultDefName == "RenderSuccessResponse" {
@@ -104,11 +97,3 @@ func (openapi *OpenAPI) CreateAndWrite(path string) error {
return os.WriteFile(path, spec, 0o600)
}
func registerQueryRoutes(router *mux.Router) {
router.Handle("/api/v5/query_range", handler.New(
func(http.ResponseWriter, *http.Request) {},
querier.QueryRangeV5OpenAPIDef,
)).Methods(http.MethodPost)
}

View File

@@ -10,35 +10,10 @@ import (
"github.com/SigNoz/signoz/pkg/types/metrictypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/swaggest/jsonschema-go"
)
type Step struct{ time.Duration }
var _ jsonschema.Exposer = Step{}
// JSONSchema returns a custom schema for Step that accepts either a duration string or a number (seconds).
func (Step) JSONSchema() (jsonschema.Schema, error) {
s := jsonschema.Schema{}
s.WithDescription("Step interval. Accepts a Go duration string (e.g., \"60s\", \"1m\", \"1h\") or a number representing seconds (e.g., 60).")
strSchema := jsonschema.Schema{}
strSchema.WithType(jsonschema.String.Type())
strSchema.WithExamples("60s", "5m", "1h")
strSchema.WithDescription("Duration string (e.g., \"60s\", \"5m\", \"1h\").")
numSchema := jsonschema.Schema{}
numSchema.WithType(jsonschema.Number.Type())
numSchema.WithExamples(60, 300, 3600)
numSchema.WithDescription("Duration in seconds.")
s.OneOf = []jsonschema.SchemaOrBool{
strSchema.ToSchemaOrBool(),
numSchema.ToSchemaOrBool(),
}
return s, nil
}
func (s *Step) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
return nil
@@ -195,14 +170,6 @@ var (
OrderDirectionDesc = OrderDirection{valuer.NewString("desc")}
)
// Enum returns the acceptable values for OrderDirection.
func (OrderDirection) Enum() []any {
return []any{
OrderDirectionAsc,
OrderDirectionDesc,
}
}
var (
OrderDirectionMap = map[string]OrderDirection{
"asc": OrderDirectionAsc,
@@ -225,19 +192,6 @@ var (
ReduceToMedian = ReduceTo{valuer.NewString("median")}
)
// Enum returns the acceptable values for ReduceTo.
func (ReduceTo) Enum() []any {
return []any{
ReduceToSum,
ReduceToCount,
ReduceToAvg,
ReduceToMin,
ReduceToMax,
ReduceToLast,
ReduceToMedian,
}
}
// FunctionReduceTo applies the reduceTo operator to a time series and returns a new series with the reduced value
// reduceTo can be one of: last, sum, avg, min, max, count, median
// if reduceTo is not recognized, the function returns the original series

View File

@@ -36,30 +36,6 @@ var (
FunctionNameFillZero = FunctionName{valuer.NewString("fillZero")}
)
// Enum returns the acceptable values for FunctionName.
func (FunctionName) Enum() []any {
return []any{
FunctionNameCutOffMin,
FunctionNameCutOffMax,
FunctionNameClampMin,
FunctionNameClampMax,
FunctionNameAbsolute,
FunctionNameRunningDiff,
FunctionNameLog2,
FunctionNameLog10,
FunctionNameCumulativeSum,
FunctionNameEWMA3,
FunctionNameEWMA5,
FunctionNameEWMA7,
FunctionNameMedian3,
FunctionNameMedian5,
FunctionNameMedian7,
FunctionNameTimeShift,
FunctionNameAnomaly,
FunctionNameFillZero,
}
}
// Validate checks if the FunctionName is valid and one of the known types
func (fn FunctionName) Validate() error {
validFunctions := []FunctionName{

View File

@@ -16,17 +16,6 @@ var (
JoinTypeCross = JoinType{valuer.NewString("cross")}
)
// Enum returns the acceptable values for JoinType.
func (JoinType) Enum() []any {
return []any{
JoinTypeInner,
JoinTypeLeft,
JoinTypeRight,
JoinTypeFull,
JoinTypeCross,
}
}
type QueryRef struct {
Name string `json:"name"`
}

View File

@@ -2,8 +2,6 @@ package querybuildertypesv5
import (
"context"
"github.com/swaggest/jsonschema-go"
)
type Query interface {
@@ -31,12 +29,4 @@ type ExecStats struct {
StepIntervals map[string]uint64 `json:"stepIntervals,omitempty"`
}
var _ jsonschema.Preparer = &ExecStats{}
// PrepareJSONSchema adds description to the ExecStats schema.
func (e *ExecStats) PrepareJSONSchema(schema *jsonschema.Schema) error {
schema.WithDescription("Execution statistics for the query, including rows scanned, bytes scanned, and duration.")
return nil
}
type TimeRange struct{ From, To uint64 } // ms since epoch

View File

@@ -16,17 +16,3 @@ var (
QueryTypeClickHouseSQL = QueryType{valuer.NewString("clickhouse_sql")}
QueryTypePromQL = QueryType{valuer.NewString("promql")}
)
// Enum returns the acceptable values for QueryType.
func (QueryType) Enum() []any {
return []any{
QueryTypeBuilder,
QueryTypeFormula,
// Not yet supported.
// QueryTypeSubQuery,
// QueryTypeJoin,
QueryTypeTraceOperator,
QueryTypeClickHouseSQL,
QueryTypePromQL,
}
}

View File

@@ -9,7 +9,6 @@ import (
"github.com/SigNoz/signoz/pkg/types/metrictypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/swaggest/jsonschema-go"
)
type QueryEnvelope struct {
@@ -19,71 +18,6 @@ type QueryEnvelope struct {
Spec any `json:"spec"`
}
// queryEnvelopeBuilderTrace is the OpenAPI schema for a QueryEnvelope with type=builder_query and signal=traces.
type queryEnvelopeBuilderTrace struct {
Type QueryType `json:"type" description:"The type of the query."`
Spec QueryBuilderQuery[TraceAggregation] `json:"spec" description:"The trace builder query specification."`
}
// queryEnvelopeBuilderLog is the OpenAPI schema for a QueryEnvelope with type=builder_query and signal=logs.
type queryEnvelopeBuilderLog struct {
Type QueryType `json:"type" description:"The type of the query."`
Spec QueryBuilderQuery[LogAggregation] `json:"spec" description:"The log builder query specification."`
}
// queryEnvelopeBuilderMetric is the OpenAPI schema for a QueryEnvelope with type=builder_query and signal=metrics.
type queryEnvelopeBuilderMetric struct {
Type QueryType `json:"type" description:"The type of the query."`
Spec QueryBuilderQuery[MetricAggregation] `json:"spec" description:"The metric builder query specification."`
}
// queryEnvelopeFormula is the OpenAPI schema for a QueryEnvelope with type=builder_formula.
type queryEnvelopeFormula struct {
Type QueryType `json:"type" description:"The type of the query."`
Spec QueryBuilderFormula `json:"spec" description:"The formula specification."`
}
// queryEnvelopeJoin is the OpenAPI schema for a QueryEnvelope with type=builder_join.
// type queryEnvelopeJoin struct {
// Type QueryType `json:"type" description:"The type of the query."`
// Spec QueryBuilderJoin `json:"spec" description:"The join specification."`
// }
// queryEnvelopeTraceOperator is the OpenAPI schema for a QueryEnvelope with type=builder_trace_operator.
type queryEnvelopeTraceOperator struct {
Type QueryType `json:"type" description:"The type of the query."`
Spec QueryBuilderTraceOperator `json:"spec" description:"The trace operator specification."`
}
// queryEnvelopePromQL is the OpenAPI schema for a QueryEnvelope with type=promql.
type queryEnvelopePromQL struct {
Type QueryType `json:"type" description:"The type of the query."`
Spec PromQuery `json:"spec" description:"The PromQL query specification."`
}
// queryEnvelopeClickHouseSQL is the OpenAPI schema for a QueryEnvelope with type=clickhouse_sql.
type queryEnvelopeClickHouseSQL struct {
Type QueryType `json:"type" description:"The type of the query."`
Spec ClickHouseQuery `json:"spec" description:"The ClickHouse SQL query specification."`
}
var _ jsonschema.OneOfExposer = QueryEnvelope{}
// JSONSchemaOneOf returns the oneOf variants for the QueryEnvelope discriminated union.
// Each variant represents a different query type with its corresponding spec schema.
func (QueryEnvelope) JSONSchemaOneOf() []any {
return []any{
queryEnvelopeBuilderTrace{},
queryEnvelopeBuilderLog{},
queryEnvelopeBuilderMetric{},
queryEnvelopeFormula{},
// queryEnvelopeJoin{},
queryEnvelopeTraceOperator{},
queryEnvelopePromQL{},
queryEnvelopeClickHouseSQL{},
}
}
// implement custom json unmarshaler for the QueryEnvelope
func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
var shadow struct {
@@ -196,12 +130,6 @@ type CompositeQuery struct {
Queries []QueryEnvelope `json:"queries"`
}
// PrepareJSONSchema adds description to the CompositeQuery schema.
func (c *CompositeQuery) PrepareJSONSchema(schema *jsonschema.Schema) error {
schema.WithDescription("Composite query containing one or more query envelopes. Each query envelope specifies its type and corresponding spec.")
return nil
}
// UnmarshalJSON implements custom JSON unmarshaling to provide better error messages
func (c *CompositeQuery) UnmarshalJSON(data []byte) error {
type Alias CompositeQuery
@@ -264,16 +192,6 @@ var (
TextBoxVariableType = VariableType{valuer.NewString("text")}
)
// Enum returns the acceptable values for VariableType.
func (VariableType) Enum() []any {
return []any{
QueryVariableType,
DynamicVariableType,
CustomVariableType,
TextBoxVariableType,
}
}
type VariableItem struct {
Type VariableType `json:"type"`
Value any `json:"value"`
@@ -299,482 +217,6 @@ type QueryRangeRequest struct {
FormatOptions *FormatOptions `json:"formatOptions,omitempty"`
}
// PrepareJSONSchema adds examples and description to the QueryRangeRequest schema.
func (q *QueryRangeRequest) PrepareJSONSchema(schema *jsonschema.Schema) error {
schema.WithDescription("Request body for the v5 query range endpoint. Supports builder queries (traces, logs, metrics), formulas, joins, trace operators, PromQL, and ClickHouse SQL queries.")
schema.WithExamples(
// 1. time_series + traces builder: count spans grouped by service, ordered by count
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "traces",
"aggregations": []any{
map[string]any{
"expression": "count()",
"alias": "span_count",
},
},
"stepInterval": "60s",
"filter": map[string]any{
"expression": "service.name = 'frontend'",
},
"groupBy": []any{
map[string]any{
"name": "service.name",
"fieldContext": "resource",
},
},
"order": []any{
map[string]any{
"key": map[string]any{"name": "span_count"},
"direction": "desc",
},
},
"limit": 10,
},
},
},
},
},
// 2. time_series + logs builder: count logs grouped by service
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "logs",
"aggregations": []any{
map[string]any{
"expression": "count()",
"alias": "log_count",
},
},
"stepInterval": "60s",
"filter": map[string]any{
"expression": "severity_text = 'ERROR'",
},
"groupBy": []any{
map[string]any{
"name": "service.name",
"fieldContext": "resource",
},
},
"order": []any{
map[string]any{
"key": map[string]any{"name": "log_count"},
"direction": "desc",
},
},
"limit": 10,
},
},
},
},
},
// 3. time_series + metrics builder (Gauge): latest value averaged across series
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "metrics",
"aggregations": []any{
map[string]any{
"metricName": "system.cpu.utilization",
"timeAggregation": "latest",
"spaceAggregation": "avg",
},
},
"stepInterval": "60s",
"groupBy": []any{
map[string]any{
"name": "host.name",
"fieldContext": "resource",
},
},
},
},
},
},
},
// 4. time_series + metrics builder (Sum): rate of cumulative counter
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "metrics",
"aggregations": []any{
map[string]any{
"metricName": "http.server.duration.count",
"timeAggregation": "rate",
"spaceAggregation": "sum",
},
},
"stepInterval": 120,
"groupBy": []any{
map[string]any{
"name": "service.name",
"fieldContext": "resource",
},
},
},
},
},
},
},
// 5. time_series + metrics builder (Histogram): p99 latency
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "metrics",
"aggregations": []any{
map[string]any{
"metricName": "http.server.duration.bucket",
"spaceAggregation": "p99",
},
},
"stepInterval": "60s",
"groupBy": []any{
map[string]any{
"name": "service.name",
"fieldContext": "resource",
},
},
},
},
},
},
},
// 6. raw + logs builder: fetch raw log records
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "raw",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "logs",
"filter": map[string]any{
"expression": "severity_text = 'ERROR'",
},
"selectFields": []any{
map[string]any{
"name": "body",
"fieldContext": "log",
},
map[string]any{
"name": "service.name",
"fieldContext": "resource",
},
},
"order": []any{
map[string]any{
"key": map[string]any{"name": "timestamp", "fieldContext": "log"},
"direction": "desc",
},
map[string]any{
"key": map[string]any{"name": "id"},
"direction": "desc",
},
},
"limit": 50,
"offset": 0,
},
},
},
},
},
// 7. raw + traces builder: fetch raw span records
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "raw",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "traces",
"filter": map[string]any{
"expression": "service.name = 'frontend' AND has_error = true",
},
"selectFields": []any{
map[string]any{
"name": "name",
"fieldContext": "span",
},
map[string]any{
"name": "duration_nano",
"fieldContext": "span",
},
},
"order": []any{
map[string]any{
"key": map[string]any{"name": "timestamp", "fieldContext": "span"},
"direction": "desc",
},
},
"limit": 100,
},
},
},
},
},
// 8. scalar + traces builder: total span count as a single value
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "scalar",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "traces",
"aggregations": []any{
map[string]any{
"expression": "count()",
"alias": "span_count",
},
},
"filter": map[string]any{
"expression": "service.name = 'frontend'",
},
},
},
},
},
},
// 9. scalar + logs builder: total error log count as a single value
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "scalar",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "logs",
"aggregations": []any{
map[string]any{
"expression": "count()",
"alias": "error_count",
},
},
"filter": map[string]any{
"expression": "severity_text = 'ERROR'",
},
},
},
},
},
},
// 10. scalar + metrics builder: single reduced value with reduceTo
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "scalar",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "metrics",
"aggregations": []any{
map[string]any{
"metricName": "http.server.duration.count",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "sum",
},
},
"stepInterval": "60s",
},
},
},
},
},
// 11. builder formula: error rate from two trace queries
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "traces",
"aggregations": []any{
map[string]any{
"expression": "countIf(has_error = true)",
},
},
"stepInterval": "60s",
"groupBy": []any{
map[string]any{
"name": "service.name",
"fieldContext": "resource",
},
},
},
},
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "B",
"signal": "traces",
"aggregations": []any{
map[string]any{
"expression": "count()",
},
},
"stepInterval": "60s",
"groupBy": []any{
map[string]any{
"name": "service.name",
"fieldContext": "resource",
},
},
},
},
map[string]any{
"type": "builder_formula",
"spec": map[string]any{
"name": "error_rate",
"expression": "A / B * 100",
},
},
},
},
},
// 12. PromQL query with UTF-8 dot metric name
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "promql",
"spec": map[string]any{
"name": "request_rate",
"query": "sum(rate({\"http.server.duration.count\"}[5m])) by (\"service.name\")",
"step": 60,
},
},
},
},
},
// 13. ClickHouse SQL — time_series
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "clickhouse_sql",
"spec": map[string]any{
"name": "span_rate",
"query": "SELECT toStartOfInterval(timestamp, INTERVAL 60 SECOND) AS ts, count() AS value FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= $start_datetime AND timestamp <= $end_datetime GROUP BY ts ORDER BY ts",
},
},
},
},
},
// 14. ClickHouse SQL — raw
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "raw",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "clickhouse_sql",
"spec": map[string]any{
"name": "recent_errors",
"query": "SELECT timestamp, body FROM signoz_logs.distributed_logs_v2 WHERE timestamp >= $start_timestamp_nano AND timestamp <= $end_timestamp_nano AND severity_text = 'ERROR' ORDER BY timestamp DESC LIMIT 100",
},
},
},
},
},
// 15. ClickHouse SQL — scalar
map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "scalar",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "clickhouse_sql",
"spec": map[string]any{
"name": "total_spans",
"query": "SELECT count() AS value FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= $start_datetime AND timestamp <= $end_datetime",
},
},
},
},
},
)
return nil
}
func (r *QueryRangeRequest) StepIntervalForQuery(name string) int64 {
stepsMap := make(map[string]int64)
for _, query := range r.CompositeQuery.Queries {

View File

@@ -30,15 +30,3 @@ var (
func (r RequestType) IsAggregation() bool {
return r == RequestTypeTimeSeries || r == RequestTypeScalar || r == RequestTypeDistribution
}
// Enum implements jsonschema.Enum; returns the acceptable values for RequestType.
func (RequestType) Enum() []any {
return []any{
RequestTypeScalar,
RequestTypeTimeSeries,
RequestTypeRaw,
RequestTypeRawStream,
RequestTypeTrace,
// RequestTypeDistribution,
}
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/swaggest/jsonschema-go"
)
type QBEvent struct {
@@ -43,17 +42,6 @@ type QueryData struct {
Results []any `json:"results"`
}
var _ jsonschema.OneOfExposer = QueryData{}
// JSONSchemaOneOf documents the polymorphic result types in QueryData.Results.
func (QueryData) JSONSchemaOneOf() []any {
return []any{
TimeSeriesData{},
ScalarData{},
RawData{},
}
}
type QueryRangeResponse struct {
Type RequestType `json:"type"`
Data QueryData `json:"data"`
@@ -64,14 +52,6 @@ type QueryRangeResponse struct {
QBEvent *QBEvent `json:"-"`
}
var _ jsonschema.Preparer = &QueryRangeResponse{}
// PrepareJSONSchema adds description to the QueryRangeResponse schema.
func (q *QueryRangeResponse) PrepareJSONSchema(schema *jsonschema.Schema) error {
schema.WithDescription("Response from the v5 query range endpoint. The data.results array contains typed results depending on the requestType: TimeSeriesData for time_series, ScalarData for scalar, or RawData for raw requests.")
return nil
}
type TimeSeriesData struct {
QueryName string `json:"queryName"`
Aggregations []*AggregationBucket `json:"aggregations"`
@@ -179,14 +159,6 @@ var (
ColumnTypeAggregation = ColumnType{valuer.NewString("aggregation")}
)
// Enum returns the acceptable values for ColumnType.
func (ColumnType) Enum() []any {
return []any{
ColumnTypeGroup,
ColumnTypeAggregation,
}
}
type ColumnDescriptor struct {
telemetrytypes.TelemetryFieldKey
QueryName string `json:"queryName"`

View File

@@ -172,18 +172,3 @@ func isContextValidForSignal(ctx FieldContext, signal Signal) bool {
}
return true
}
// Enum returns the acceptable values for FieldContext.
func (FieldContext) Enum() []any {
return []any{
FieldContextMetric,
FieldContextLog,
FieldContextSpan,
// FieldContextTrace,
FieldContextResource,
// FieldContextScope,
FieldContextAttribute,
// FieldContextEvent,
FieldContextBody,
}
}

View File

@@ -169,19 +169,3 @@ func (f FieldDataType) TagDataType() string {
return "string"
}
}
// Enum returns the acceptable values for FieldDataType.
func (FieldDataType) Enum() []any {
return []any{
FieldDataTypeString,
FieldDataTypeBool,
FieldDataTypeFloat64,
FieldDataTypeInt64,
FieldDataTypeNumber,
// FieldDataTypeArrayString,
// FieldDataTypeArrayFloat64,
// FieldDataTypeArrayBool,
// FieldDataTypeArrayInt64,
// FieldDataTypeArrayNumber,
}
}

View File

@@ -12,12 +12,3 @@ var (
SignalMetrics = Signal{valuer.NewString("metrics")}
SignalUnspecified = Signal{valuer.NewString("")}
)
// Enum returns the acceptable values for Signal.
func (Signal) Enum() []any {
return []any{
SignalTraces,
SignalLogs,
SignalMetrics,
}
}

View File

@@ -10,10 +10,3 @@ var (
SourceMeter = Source{valuer.NewString("meter")}
SourceUnspecified = Source{valuer.NewString("")}
)
// Enum returns the acceptable values for Source.
func (Source) Enum() []any {
return []any{
SourceMeter,
}
}

View File

@@ -1,62 +0,0 @@
from http import HTTPStatus
from typing import Callable
import requests
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.types import SigNoz
DUPLICATE_USER_EMAIL = "duplicate@integration.test"
def test_duplicate_user_invite_rejected(
signoz: SigNoz,
get_token: Callable[[str, str], str],
):
"""
Verify that the unique index on (email, org_id) in the users table prevents
creating duplicate users. This invites a new user, accepts the invite, then
tries to invite and accept the same email again expecting a failure.
"""
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Step 1: Invite a new user.
initial_invite_response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/invite"),
json={"email": DUPLICATE_USER_EMAIL, "role": "EDITOR"},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
assert initial_invite_response.status_code == HTTPStatus.CREATED
initial_invite_token = initial_invite_response.json()["data"]["token"]
# Step 2: Accept the invite to create the user.
initial_accept_response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/invite/accept"),
json={"token": initial_invite_token, "password": "password123Z$"},
timeout=2,
)
assert initial_accept_response.status_code == HTTPStatus.CREATED
# Step 3: Invite the same email again.
duplicate_invite_response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/invite"),
json={"email": DUPLICATE_USER_EMAIL, "role": "VIEWER"},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=2,
)
# The invite creation itself may be rejected if the app checks for existing users.
if duplicate_invite_response.status_code != HTTPStatus.CREATED:
assert duplicate_invite_response.status_code == HTTPStatus.CONFLICT
return
duplicate_invite_token = duplicate_invite_response.json()["data"]["token"]
# Step 4: Accept the duplicate invite — should fail due to unique constraint.
duplicate_accept_response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/invite/accept"),
json={"token": duplicate_invite_token, "password": "password123Z$"},
timeout=2,
)
assert duplicate_accept_response.status_code == HTTPStatus.CONFLICT