mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-18 15:02:35 +00:00
Compare commits
6 Commits
query-rang
...
SIG-3497
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f0f5b51a9 | ||
|
|
037b92feb3 | ||
|
|
95e023dafd | ||
|
|
d20c866d2c | ||
|
|
06f55e6eda | ||
|
|
f592bc084d |
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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'>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"> </div>
|
||||
</div>
|
||||
<div className="log-overflow-shadow"> </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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
.metrics-search-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.metrics-search-options {
|
||||
display: flex;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user