mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-05 12:40:46 +01:00
Compare commits
1 Commits
issue_5601
...
v2-transpi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
723391c4a2 |
3
.github/workflows/integrationci.yaml
vendored
3
.github/workflows/integrationci.yaml
vendored
@@ -39,8 +39,6 @@ jobs:
|
||||
matrix:
|
||||
suite:
|
||||
- alerts
|
||||
- alertmanager
|
||||
- alertmanagerrotation
|
||||
- basepath
|
||||
- callbackauthn
|
||||
- cloudintegrations
|
||||
@@ -55,7 +53,6 @@ jobs:
|
||||
- queriermetrics
|
||||
- querierscalar
|
||||
- queriercommon
|
||||
- querierai
|
||||
- rawexportdata
|
||||
- promqlconformance
|
||||
- querierauthz
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -6902,7 +6902,6 @@ components:
|
||||
Querybuildertypesv5QueryEnvelope:
|
||||
discriminator:
|
||||
mapping:
|
||||
builder_ai_query: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilderAI'
|
||||
builder_formula: '#/components/schemas/Querybuildertypesv5QueryEnvelopeFormula'
|
||||
builder_query: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilder'
|
||||
builder_trace_operator: '#/components/schemas/Querybuildertypesv5QueryEnvelopeTraceOperator'
|
||||
@@ -6911,7 +6910,6 @@ components:
|
||||
propertyName: type
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilder'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilderAI'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeFormula'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeTraceOperator'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopePromQL'
|
||||
@@ -6926,15 +6924,6 @@ components:
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
Querybuildertypesv5QueryEnvelopeBuilderAI:
|
||||
properties:
|
||||
spec:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
|
||||
type:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
Querybuildertypesv5QueryEnvelopeClickHouseSQL:
|
||||
properties:
|
||||
spec:
|
||||
@@ -7048,7 +7037,6 @@ components:
|
||||
Querybuildertypesv5QueryType:
|
||||
enum:
|
||||
- builder_query
|
||||
- builder_ai_query
|
||||
- builder_formula
|
||||
- builder_trace_operator
|
||||
- clickhouse_sql
|
||||
@@ -15489,72 +15477,6 @@ paths:
|
||||
summary: Lock dashboard (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/dashboards/{id}/migrate:
|
||||
post:
|
||||
deprecated: false
|
||||
description: 'This endpoint retries the v1→v2 (Perses) migration on a dashboard
|
||||
still stored in the v1 schema and returns the v2-shape result. It is idempotent:
|
||||
a dashboard already in the v2 schema is returned unchanged.'
|
||||
operationId: MigrateDashboardV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
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
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Migrate dashboard to v2
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/factor_password/forgot:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
This document is the subsystem context for `pkg/prometheus/clickhouseprometheusv2`,
|
||||
the second-generation ClickHouse-backed Prometheus provider. It explains why the
|
||||
package exists, the correctness constraints that shaped it, and how each fetch
|
||||
reduction is proven not to change results. Any change to the provider must keep
|
||||
package exists, the correctness constraints that shaped it, and how each
|
||||
construct is proven not to change results. Any change to the provider must keep
|
||||
these invariants; if a change would violate one, it must be flagged and
|
||||
discussed.
|
||||
|
||||
@@ -17,107 +17,337 @@ window is fetched, serialized, and handed to the engine. The cost is a function
|
||||
of ingested data, not of the question asked — which is how a dashboard of PromQL
|
||||
panels can take an instance down.
|
||||
|
||||
In v2 the stock promql engine evaluates over a native `storage.Querier`: no
|
||||
translation layer, per-selector fetch windows, and fetch reductions that are
|
||||
provably invisible to the engine.
|
||||
In v2, every query runs in one of two ways, decided per query:
|
||||
|
||||
**The core constraint: every reduction either preserves engine semantics exactly
|
||||
or is not performed.** A PromQL result that differs from upstream Prometheus is
|
||||
a lost user. The conformance suite
|
||||
- **Transpiled**: the query is evaluated entirely inside ClickHouse and only
|
||||
final (or near-final) per-group grid arrays come back, built on the
|
||||
`timeSeries*ToGrid` aggregate functions (the supported ClickHouse floor is
|
||||
>= 25.6, so they are assumed available).
|
||||
- **Engine**: the stock promql engine evaluates over this package's native
|
||||
`storage.Querier`. This is the path for everything not transpilable.
|
||||
|
||||
**The core constraint: a PromQL result that differs from upstream Prometheus is
|
||||
a lost user, so anything that cannot reproduce engine semantics exactly falls
|
||||
back rather than approximate.** The conformance suite
|
||||
(`tests/integration/tests/promqlconformance/`) replays Prometheus' own test
|
||||
corpus against both providers and is the arbiter.
|
||||
corpus against both providers and is the arbiter; the classification golden
|
||||
(`testdata/classification_golden.json`) freezes which of the two ways each
|
||||
corpus expression takes. The rest of this document is the PromQL -> SQL story,
|
||||
because that mapping is where correctness is won or lost.
|
||||
|
||||
---
|
||||
|
||||
## The evaluation model the SQL must reproduce
|
||||
|
||||
A PromQL range query is an instant query evaluated at every grid point
|
||||
t_i = start + i*step, i = 0..(end-start)/step. At each t_i:
|
||||
|
||||
- an instant selector resolves to the latest sample in the left-open
|
||||
lookback window (t_i - lookback, t_i], and to nothing when that latest
|
||||
sample is a stale marker — even if older real samples sit inside the
|
||||
window;
|
||||
- a range selector [r] collects every sample in (t_i - r, t_i], stale
|
||||
markers excluded;
|
||||
- offset d shifts both windows to (t_i - d - w, t_i - d].
|
||||
|
||||
The transpilation invariant follows from this: every transpiled construct
|
||||
produces, per output series, one array with exactly one slot per grid
|
||||
point — slot i holds the value at t_i, NULL means absent. This is what
|
||||
makes composition correct, not just convenient: the engine evaluates
|
||||
these operators independently per t_i, so any representation that gets
|
||||
every slot right gets the whole query right, and spatial aggregation over
|
||||
arrays is sound because it combines values that belong to the same t_i by
|
||||
construction. Slot index i maps back to t_i = start + i*step at scan time
|
||||
(toMatrix). Everything below is about filling those slots with exactly
|
||||
the numbers the engine would compute — and each equivalence was validated
|
||||
against the vendored engine on live data before its shape entered the
|
||||
allowlist; anything unproven stays on the engine path.
|
||||
|
||||
## Classification: finding what a statement can answer
|
||||
|
||||
classify walks the parsed AST looking for "core units" — maximal subtrees
|
||||
of the shape
|
||||
|
||||
[agg by/without (...)] [fn(] selector[range] [offset d] [)] [op scalar]...
|
||||
|
||||
classifyCore peels that chain from the outside in: an optional
|
||||
sum/min/max/avg/count aggregation, then one of the allowlisted functions
|
||||
or a bare instant selector, then the selector with its offset; on the way
|
||||
out it accumulates number-literal arithmetic, comparisons (including
|
||||
bool) and unary minus into a scalar-op pipeline. A node qualifies only if
|
||||
its type, arguments and children are in the proven set — an allowlist, so
|
||||
an overlooked construct becomes a fallback instead of a wrong number.
|
||||
|
||||
Three unit kinds come out of this, each with its own SQL form:
|
||||
unitRange (rate, irate, increase, delta, idelta over a range selector),
|
||||
unitInstant (instant vector selection, bare or comparison-filtered) and
|
||||
unitOverTime (avg/min/max/sum/count/last _over_time).
|
||||
|
||||
If the entire tree is one unit, the plan is "full": the statement's rows
|
||||
are the query result. Otherwise every maximal unit is cut out and replaced
|
||||
in the expression with a synthetic selector __signoz_transpiled_N__, and
|
||||
the rewritten expression runs in the engine over the units' materialized
|
||||
results ("hybrid") — histogram_quantile, topk, or/and/unless and vector
|
||||
matching keep exact engine semantics while their expensive inputs were
|
||||
aggregated server-side.
|
||||
|
||||
Classification refuses when exact semantics cannot be guaranteed
|
||||
server-side: the @ modifier anywhere and default-resolution subqueries
|
||||
(their resolution is a server runtime setting the transpiler cannot see);
|
||||
duration expressions (offset step(), [range()], ...) anywhere — they are
|
||||
resolved into the selector's static fields only at evaluation time, so at
|
||||
classification time those fields still hold their zero values and
|
||||
transpiling would silently use the wrong offset or range;
|
||||
steps or ranges that are not whole seconds (the grid functions take
|
||||
whole-second parameters); grouping by or matching on __name__ in hybrid
|
||||
plans (the synthetic name would leak into results); name-keeping units —
|
||||
bare/comparison instant selectors and last_over_time keep their real
|
||||
__name__ (keepsName), which substitution would replace, so they transpile
|
||||
only as full plans; and every function outside the allowlist (changes,
|
||||
resets, quantile_over_time, absent, native-histogram functions, ...).
|
||||
|
||||
Units inside a fixed-resolution subquery evaluate on the subquery's own
|
||||
grid instead of the query grid: epoch-aligned multiples of the resolution
|
||||
strictly after outerStart - offset - range, ending at outer end - offset —
|
||||
the exact derivation the engine uses, because a grid shifted by one step
|
||||
changes which samples every window sees.
|
||||
|
||||
## From one unit to one statement
|
||||
|
||||
buildUnitSQL renders each unit as a single statement. For
|
||||
sum by (pod) (rate(m{job="api"}[5m])) the skeleton is:
|
||||
|
||||
SELECT gkey, sumForEach(grid) AS grid FROM (
|
||||
SELECT series.gkey AS gkey,
|
||||
timeSeriesRateToGrid(<start>, <end>, <step>, <range>)(fromUnixTimestamp64Milli(unix_milli), value) AS grid
|
||||
FROM signoz_metrics.distributed_samples_v4 AS points
|
||||
INNER JOIN (
|
||||
SELECT fingerprint, <group key expr> AS gkey
|
||||
FROM signoz_metrics.time_series_v4
|
||||
WHERE <series predicates>
|
||||
GROUP BY fingerprint, gkey
|
||||
) AS series ON points.fingerprint = series.fingerprint
|
||||
WHERE metric_name = ? AND temporality IN ['Cumulative', 'Unspecified']
|
||||
AND points.fingerprint IN (<matched fingerprints>)
|
||||
AND unix_milli > <start - range> AND unix_milli <= <end>
|
||||
AND bitAnd(flags, 1) = 0
|
||||
GROUP BY points.fingerprint, series.gkey
|
||||
) GROUP BY gkey
|
||||
SETTINGS allow_experimental_ts_to_grid_aggregate_function = 1
|
||||
|
||||
Reading it inside out:
|
||||
|
||||
The time window is the selector's semantics verbatim: strict > on the
|
||||
lower bound and <= on the upper is the left-open (t - w, t] rule, with the
|
||||
whole window shifted by the offset. bitAnd(flags, 1) = 0 drops stale
|
||||
markers, which PromQL excludes from range vectors.
|
||||
|
||||
The inner GROUP BY computes one grid array per series.
|
||||
timeSeriesRateToGrid(start, end, step, range) is a parametric aggregate:
|
||||
fed (timestamp, value) pairs it produces Array(Nullable(Float64)) with one
|
||||
slot per grid point. Correct because it implements the engine's
|
||||
extrapolatedRate decision for decision — counter resets, the zero-point
|
||||
clamp, the extrapolation thresholds, the >= 2 samples rule, the left-open
|
||||
window — verified by feeding identical samples to both and comparing
|
||||
slot for slot: the only difference ever observed is the last bit
|
||||
(ClickHouse's C++ and Go round the same formula differently), which is
|
||||
the floating-point floor, not a semantic gap. irate/delta/idelta map to
|
||||
their own timeSeries*ToGrid functions with the same verification;
|
||||
increase has no function of its own and is emitted as
|
||||
arrayMap(x -> x * <range seconds>, <rate expr>), exact by definition —
|
||||
extrapolatedRate computes the same extrapolated delta for both and
|
||||
divides by the range only when isRate, so multiplying it back is the
|
||||
identity, not an approximation. The grid parameters are rendered as
|
||||
literals, not bound args — they are aggregate-function parameters — and
|
||||
the experimental gate rides as a SETTINGS clause on the statement itself
|
||||
so telemetrystore hooks cannot clobber it.
|
||||
|
||||
The join annotates each series with its group key, in one of two forms.
|
||||
by (...) extracts each listed label as a plain column
|
||||
(JSONExtractString(labels, 'pod') AS g0) and groups on the columns
|
||||
directly: the projection is a known short list and the label names live
|
||||
in Go, so building, sorting and stringifying every label pair per row
|
||||
would be waste. Correct because column-tuple equality is label-set
|
||||
equality on the projection, and an extracted '' is the label being
|
||||
absent — Prometheus semantics for by() over missing labels, and empties
|
||||
are skipped when the columns turn back into labels. without and
|
||||
no-aggregation project a label SET that varies per series, so they get
|
||||
the canonical key: toJSONString of the sorted [label, value] pairs the
|
||||
unit projects (without excludes the listed labels plus __name__; no
|
||||
aggregation keeps everything, the name coming off in Go per the engine's
|
||||
name-dropping rules). There the sort is load-bearing — stored JSON key
|
||||
order is not canonical across fingerprints, and two orderings of the same
|
||||
labels must land in one group — empty values are filtered for the same
|
||||
absent-label reason, and the same string parses back into the output
|
||||
label set (labelsFromGroupKey).
|
||||
|
||||
The outer GROUP BY is the spatial aggregation: sum/min/max/avg/count
|
||||
by/without become the -ForEach combinators. Element-wise aggregation over
|
||||
grid arrays is the engine's per-t_i aggregation, because slot i of every
|
||||
input array refers to the same t_i; the combinators skip NULLs, which is
|
||||
the engine aggregating only the series present at t_i, and an index where
|
||||
every series is absent stays NULL. Two edges need explicit handling:
|
||||
countForEach wraps in a mapping of 0 back to NULL, because a count over
|
||||
an all-absent index is an absent point, not 0; and a unit without
|
||||
aggregation still passes through maxForEach — the identity for the common
|
||||
one-fingerprint group, and a deterministic NULL-skipping merge when a
|
||||
regex __name__ selector collapses distinct metrics onto one projected
|
||||
label set. One caveat is inherent: summation order over series differs
|
||||
from the engine's, so spatial aggregates can differ in the last ULP —
|
||||
float addition is not associative; no ordering reproduces the engine's
|
||||
bit-exactly from inside a GROUP BY.
|
||||
|
||||
## Instant selectors: staleness needs two aggregates
|
||||
|
||||
unitInstant uses window = lookback and must reproduce the shadowing rule:
|
||||
the point is absent when the latest in-window sample is a stale marker.
|
||||
timeSeriesLastToGrid alone cannot express that — skipping stale rows in
|
||||
WHERE would resurrect the older real sample the marker was written to
|
||||
bury. So stale rows stay in the scan for this kind only, and the grid
|
||||
expression compares three aggregates per slot:
|
||||
|
||||
arrayMap((tall, tok, vok) -> if(tall IS NULL OR tok IS NULL OR tall != tok, NULL, vok),
|
||||
timeSeriesLastToGrid(...)(ts, toFloat64(unix_milli)), -- last sample overall
|
||||
timeSeriesLastToGridIf(...)(ts, toFloat64(unix_milli), bitAnd(flags, 1) = 0), -- last non-stale, its timestamp
|
||||
timeSeriesLastToGridIf(...)(ts, value, bitAnd(flags, 1) = 0)) -- last non-stale, its value
|
||||
|
||||
Correct by cases on a slot's window. No samples at all: both timestamp
|
||||
aggregates are NULL, the slot is NULL — absent, as the engine says. Latest
|
||||
sample non-stale: it is the latest overall and the latest non-stale, the
|
||||
timestamps agree, the slot takes its value — the engine's pick. Latest
|
||||
sample stale: the last-overall timestamp is the marker's, the
|
||||
last-non-stale timestamp is older (or NULL when only markers are in
|
||||
window), they disagree, the slot is NULL — the marker shadows, exactly
|
||||
the engine's rule. Timestamps are unique per series (ingest dedups), so
|
||||
timestamp equality identifies "the same sample" without ambiguity. The
|
||||
-If combinator's applicability to these experimental aggregates was
|
||||
probed before being trusted, not assumed.
|
||||
|
||||
## Windowed *_over_time: whole buckets instead of a grid function
|
||||
|
||||
avg/min/max/sum/count _over_time aggregate every raw sample in the window,
|
||||
and no timeSeries*ToGrid function computes them. (last_over_time is the
|
||||
exception: the last sample of a range vector — stale markers excluded from
|
||||
range vectors by PromQL, excluded here in WHERE — is exactly
|
||||
timeSeriesLastToGrid.) These transpile only when the range is a whole
|
||||
multiple of the step, and then the window needs no per-sample fan-out at
|
||||
all: with W = range/step, the window (t_k - range, t_k] is exactly the
|
||||
union of W step buckets — both are left-open on the same boundaries — so
|
||||
bucket membership fully determines window membership. Each sample lands
|
||||
in exactly one bucket by a plain GROUP BY:
|
||||
|
||||
intDiv(unix_milli - <start> + <range> - 1, <step>) AS jj
|
||||
|
||||
(ceil((ts - start)/step) shifted by W-1 so the earliest in-window sample
|
||||
sits at 0; slot k's window is buckets jj in [k, k+W-1]). The alternative —
|
||||
fanning each sample into all W windows that cover it — multiplies rows by
|
||||
W, which for a long range over a short step is a row explosion measured
|
||||
in billions; the bucketed form's row count is series x buckets, the size
|
||||
of the output, regardless of W.
|
||||
|
||||
The shard level aggregates per (series, group key, bucket): a bucket
|
||||
count plus the function's value aggregate (sum for sum/avg, min, max).
|
||||
The assembly level places the partials into dense arrays
|
||||
(groupArrayInsertAt — positions are unique, one row per bucket; counts
|
||||
and sums default to 0, which contributes nothing) and slides: slot k
|
||||
combines its at-most-W bucket partials by direct aggregation, so window
|
||||
sums are added the way the engine adds them — no prefix-sum differencing,
|
||||
whose large-minus-large cancellation would drift past the shadow
|
||||
tolerance on counter-sized values. Correct per slot because the bucket
|
||||
union is the exact window multiset and avg/min/max/sum/count are
|
||||
order-insensitive on a multiset (sum/avg up to summation order, the float
|
||||
caveat above). A slot with zero window count is absent; min/max filter
|
||||
their slices on the bucket counts, so an empty bucket's default can never
|
||||
be mistaken for a value — a real sample can legitimately be +Inf.
|
||||
|
||||
Ranges that don't divide the step, and windows wider than
|
||||
maxWindowBuckets buckets (the slide costs W combines per slot), fall back
|
||||
to the engine path, which is exact.
|
||||
|
||||
## Scalar ops, full plans, hybrid plans
|
||||
|
||||
The scalar-op pipeline applies in Go to the returned arrays
|
||||
(applyScalarOps), slot by slot: arithmetic operators compute, comparisons
|
||||
filter (the slot keeps the vector-side value or becomes NULL) or return
|
||||
0/1 under bool. Correct trivially: it is the same float64 operation the
|
||||
engine would apply to the same slot value, in the same operator order the
|
||||
AST dictates — running it in Go instead of another SQL layer changes
|
||||
where, not what.
|
||||
|
||||
A full plan's arrays map straight to the result matrix. A hybrid plan
|
||||
materializes each unit's arrays as synthetic series under its
|
||||
__signoz_transpiled_N__ name and evaluates the rewritten expression over
|
||||
a storage that serves synthetic names from memory and everything else
|
||||
live. Substitution is sound because a unit's output is a plain instant
|
||||
vector to the engine — same values at same timestamps under a different
|
||||
name, and the name cannot matter: plans that group by or match on
|
||||
__name__ were refused at classification, and name-keeping units are never
|
||||
substituted. One subtlety makes it exact: stale markers are written at
|
||||
absent grid points, because the engine's lookback would otherwise
|
||||
resurrect a point from up to lookback earlier — the marker encodes
|
||||
"absent here" the way the engine itself encodes it. Units evaluate
|
||||
concurrently; each is one series lookup plus one grid statement. A step
|
||||
of 0 is an instant query: a single evaluation at end.
|
||||
|
||||
## Series lookup
|
||||
|
||||
Matchers resolve to series once per selector (`selectSeries`) against the series
|
||||
tables, which hold one row per (fingerprint, bucket) at 1h/6h/1d/1w
|
||||
granularities. Table selection and window rounding delegate to the shared
|
||||
metrics schema package (`pkg/telemetryschema/metricstelemetryschema`); the
|
||||
window start rounds down to the bucket boundary so a window beginning mid-bucket
|
||||
still matches the bucket's row.
|
||||
Both paths resolve matchers the same way, once per selector
|
||||
(selectSeries), against the series tables holding one row per
|
||||
(fingerprint, bucket) at 1h/6h/1d/1w granularities; timeSeriesTableFor
|
||||
picks the table whose bucket fits the window and rounds the window start
|
||||
down to the bucket boundary. How matchers become SQL, and why regexes are
|
||||
anchored, is documented at applySeriesConditions. Empty-valued labels come
|
||||
off at this boundary: an empty value means "label absent" in Prometheus,
|
||||
but stored attribute JSON can carry them.
|
||||
|
||||
How matchers become SQL is documented at `applySeriesConditions`. The rules that
|
||||
carry semantics:
|
||||
## The engine path
|
||||
|
||||
- `__name__` matchers (all four types) translate to the `metric_name` column.
|
||||
- Every other matcher becomes a `JSONExtractString` condition on the labels
|
||||
column. An equality matcher against `""` matches series *without* the label,
|
||||
mirroring PromQL, because `JSONExtractString` returns `""` for missing keys.
|
||||
- Regexes are anchored (`^(?:...)$`) before they reach `match()`: PromQL
|
||||
matchers match the whole value, ClickHouse `match()` searches for a
|
||||
substring.
|
||||
- The series-lookup upper bound is inclusive (`unix_milli <= end`) because the
|
||||
exporter floors registration rows to the bucket start: a series first
|
||||
registered in the bucket beginning exactly at `end` would otherwise be
|
||||
invisible while its samples are in range.
|
||||
|
||||
Empty-valued labels come off at this boundary: an empty value means "label
|
||||
absent" in Prometheus, but stored attribute JSON can carry them.
|
||||
|
||||
---
|
||||
|
||||
## Sample fetch
|
||||
|
||||
Samples are fetched per selector using the engine's per-selector hints, not the
|
||||
query-wide union window — `foo / foo offset 1d` reads two narrow windows
|
||||
instead of the widest one twice.
|
||||
|
||||
**Last-sample-per-step reduction.** Instant selectors of subquery-free queries
|
||||
fetch only the last sample per step bucket. The engine resolves an instant
|
||||
selector at each grid timestamp `t` to the latest sample in the left-open
|
||||
lookback window `(t − lookback, t]`. Buckets anchor at the selector's first
|
||||
evaluation timestamp — recovered from the hints as
|
||||
`hints.Start + lookback − 1ms`, the inverse of how the engine derives
|
||||
`hints.Start` — so bucket boundaries coincide with evaluation timestamps, and a
|
||||
non-final sample of a bucket can never be the latest sample in
|
||||
`(t − lookback, t]` for any grid `t`. Real timestamps are preserved, so the
|
||||
engine's own lookback and staleness handling stay exact.
|
||||
|
||||
Range selectors always fetch raw — every sample feeds the range function. The
|
||||
subquery-free proof travels in the context as `prometheus.QueryTraits`, because
|
||||
subquery selectors evaluate at the subquery's step while the hints carry the
|
||||
top-level step; call sites that do not attach traits get the conservative raw
|
||||
fetch.
|
||||
|
||||
**Row assembly** maps stale flags to the engine's `StaleNaN` and merges series
|
||||
with identical label sets (`sortAndMerge`) — the engine assumes storages never
|
||||
emit duplicates. Duplicate timestamps pass through as stored: uniqueness is
|
||||
ingest's job, and v1 feeds them to the engine as-is over the same data.
|
||||
|
||||
**The fingerprint filter is a shard-local semi-join.** The samples query
|
||||
restricts to the matched series by re-running the series predicates as an
|
||||
`IN (SELECT fingerprint FROM <local series table> ...)` subquery, not a GLOBAL
|
||||
broadcast of the matched set. ClickHouse materializes the subquery's set per
|
||||
shard before the scan, so it still engages the fingerprint primary-key column.
|
||||
Because the subquery re-executes the predicates after the lookup ran, it can
|
||||
match series registered in between; sample rows whose fingerprint the lookup
|
||||
never saw are skipped — the lookup is the read snapshot.
|
||||
|
||||
---
|
||||
Queries that do not transpile run in the stock engine over this package's
|
||||
storage.Querier, which is still not the v1 path. Samples are fetched per
|
||||
selector using the engine's per-selector hints, not the query-wide union
|
||||
window, so foo / foo offset 1d reads two narrow windows instead of the
|
||||
widest one twice. Instant selectors of subquery-free queries fetch only
|
||||
the last sample per step bucket (lastSamplePerStep): buckets anchor at the
|
||||
selector's first evaluation timestamp — recovered from the hints as
|
||||
hints.Start + lookback - 1ms, the inverse of how the engine derives
|
||||
hints.Start — so bucket boundaries coincide with evaluation timestamps and
|
||||
a non-final sample of a bucket can never be the latest sample in
|
||||
(t - lookback, t] for any grid t. Real timestamps are preserved, so the
|
||||
engine's own lookback and staleness handling stay exact. Range selectors
|
||||
always fetch raw — every sample feeds the range function — and the
|
||||
subquery-free proof travels in the context as prometheus.QueryTraits,
|
||||
because subquery selectors evaluate at the subquery's step while the
|
||||
hints carry the top-level step. Row assembly maps stale flags to the
|
||||
engine's StaleNaN and merges series with identical label sets
|
||||
(sortAndMerge) — the engine assumes storages never emit duplicates.
|
||||
|
||||
## Sharding
|
||||
|
||||
`samples_v4` and `time_series_v4` (and all their rollups) shard on the same key
|
||||
— `cityHash64(env, temporality, metric_name, fingerprint)` — so a series'
|
||||
samples and catalog rows live on the same shard. The semi-join above exploits
|
||||
that: each shard filters by its own series rows, which are exactly the series
|
||||
of that shard's samples.
|
||||
|
||||
The temporality filter on every samples statement
|
||||
(`temporality IN ['Cumulative', 'Unspecified']`) is a semantic no-op — the
|
||||
matched fingerprints already come from those temporalities — that engages the
|
||||
leading samples primary-key column.
|
||||
|
||||
Delta-temporality series stay invisible to PromQL exactly as they are in v1:
|
||||
the rollout gate is parity with v1, and a Delta stream fed to `rate()`
|
||||
as-if-cumulative would be wrong, not just new.
|
||||
|
||||
---
|
||||
samples_v4 and time_series_v4 (and all their rollups) shard on the same
|
||||
key — cityHash64(env, temporality, metric_name, fingerprint) — so a
|
||||
series' samples and catalog rows live on the same shard. The transpiled
|
||||
statement above exploits that: the distributed samples table at the
|
||||
top-level FROM makes ClickHouse rewrite the whole inner query per shard,
|
||||
where the join against the shard-local series table and the per-series
|
||||
grid aggregation run next to the data; the initiator only merges
|
||||
aggregate states and applies the spatial -ForEach step. Same layout as
|
||||
the telemetrymetrics statement builder. The group-key join alone
|
||||
restricts the transpiled scan to the matched series; the engine path's
|
||||
samples fetch restricts by the same predicates as a shard-local
|
||||
semi-join, not a GLOBAL broadcast of the matched set. The temporality
|
||||
filter on every
|
||||
samples statement is a semantic no-op — the matched fingerprints already
|
||||
come from those temporalities — that engages the leading samples
|
||||
primary-key column. Delta-temporality series stay invisible to PromQL
|
||||
here exactly as they are in v1: the rollout gate is parity with v1, and
|
||||
making Delta visible is its own change with its own semantics to design —
|
||||
a Delta stream fed to rate() as-if-cumulative would be wrong, not just
|
||||
new.
|
||||
|
||||
## Observability
|
||||
|
||||
Every statement carries a `log_comment` with
|
||||
`code.namespace=clickhouse-prometheus-v2` and `code.function.name` naming the
|
||||
call site, so this provider's work is attributable in `system.query_log`.
|
||||
Every statement carries a log_comment with
|
||||
code.namespace=clickhouse-prometheus-v2 and code.function.name naming the
|
||||
call site (selectSeries, selectSamples, transpiledUnit, LabelValues,
|
||||
LabelNames), so this provider's work is attributable in system.query_log
|
||||
without guessing from query text.
|
||||
|
||||
@@ -276,10 +276,6 @@ func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UU
|
||||
return module.pkgDashboardModule.GetV2(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) MigrateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.MigrateV2(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatable dashboardtypes.UpdatableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.UpdateV2(ctx, orgID, id, updatedBy, updatable)
|
||||
}
|
||||
|
||||
@@ -80,6 +80,15 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
Route: "",
|
||||
})
|
||||
|
||||
fineGrainedAuthz := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseFineGrainedAuthz, evalCtx)
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureUseFineGrainedAuthz.String()),
|
||||
Active: fineGrainedAuthz,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
})
|
||||
|
||||
aiObservability := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureEnableAIObservability, evalCtx)
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureEnableAIObservability.String()),
|
||||
@@ -98,6 +107,15 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
Route: "",
|
||||
})
|
||||
|
||||
infraMonitoringV2 := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseInfraMonitoringV2, evalCtx)
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureUseInfraMonitoringV2.String()),
|
||||
Active: infraMonitoringV2,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
})
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
|
||||
@@ -52,8 +52,6 @@ import type {
|
||||
ListDashboardsV2200,
|
||||
ListDashboardsV2Params,
|
||||
LockDashboardV2PathParameters,
|
||||
MigrateDashboardV2200,
|
||||
MigrateDashboardV2PathParameters,
|
||||
PatchDashboardV2200,
|
||||
PatchDashboardV2PathParameters,
|
||||
PinDashboardV2PathParameters,
|
||||
@@ -1806,85 +1804,6 @@ export const useLockDashboardV2 = <
|
||||
> => {
|
||||
return useMutation(getLockDashboardV2MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint retries the v1→v2 (Perses) migration on a dashboard still stored in the v1 schema and returns the v2-shape result. It is idempotent: a dashboard already in the v2 schema is returned unchanged.
|
||||
* @summary Migrate dashboard to v2
|
||||
*/
|
||||
export const migrateDashboardV2 = (
|
||||
{ id }: MigrateDashboardV2PathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<MigrateDashboardV2200>({
|
||||
url: `/api/v2/dashboards/${id}/migrate`,
|
||||
method: 'POST',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getMigrateDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof migrateDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: MigrateDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof migrateDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: MigrateDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['migrateDashboardV2'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof migrateDashboardV2>>,
|
||||
{ pathParams: MigrateDashboardV2PathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return migrateDashboardV2(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type MigrateDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof migrateDashboardV2>>
|
||||
>;
|
||||
|
||||
export type MigrateDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Migrate dashboard to v2
|
||||
*/
|
||||
export const useMigrateDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof migrateDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: MigrateDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof migrateDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: MigrateDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getMigrateDashboardV2MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint returns the sanitized v2-shape dashboard data for public access. Each panel query is reduced to a safe field subset, so filters and raw query strings are not exposed.
|
||||
* @summary Get public dashboard data (v2)
|
||||
|
||||
@@ -4301,18 +4301,6 @@ export interface Querybuildertypesv5QueryEnvelopeBuilderDTO {
|
||||
type: Querybuildertypesv5QueryEnvelopeBuilderDTOType;
|
||||
}
|
||||
|
||||
export enum Querybuildertypesv5QueryEnvelopeBuilderAIDTOType {
|
||||
builder_ai_query = 'builder_ai_query',
|
||||
}
|
||||
export interface Querybuildertypesv5QueryEnvelopeBuilderAIDTO {
|
||||
spec?: Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregationDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @enum builder_ai_query
|
||||
*/
|
||||
type: Querybuildertypesv5QueryEnvelopeBuilderAIDTOType;
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5QueryBuilderFormulaDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
@@ -4496,7 +4484,6 @@ export interface Querybuildertypesv5QueryEnvelopeClickHouseSQLDTO {
|
||||
|
||||
export type Querybuildertypesv5QueryEnvelopeDTO =
|
||||
| Querybuildertypesv5QueryEnvelopeBuilderDTO
|
||||
| Querybuildertypesv5QueryEnvelopeBuilderAIDTO
|
||||
| Querybuildertypesv5QueryEnvelopeFormulaDTO
|
||||
| Querybuildertypesv5QueryEnvelopeTraceOperatorDTO
|
||||
| Querybuildertypesv5QueryEnvelopePromQLDTO
|
||||
@@ -8300,7 +8287,6 @@ export interface Querybuildertypesv5QueryRangeResponseDTO {
|
||||
|
||||
export enum Querybuildertypesv5QueryTypeDTO {
|
||||
builder_query = 'builder_query',
|
||||
builder_ai_query = 'builder_ai_query',
|
||||
builder_formula = 'builder_formula',
|
||||
builder_trace_operator = 'builder_trace_operator',
|
||||
clickhouse_sql = 'clickhouse_sql',
|
||||
@@ -11178,17 +11164,6 @@ export type UnlockDashboardV2PathParameters = {
|
||||
export type LockDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type MigrateDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type MigrateDashboardV2200 = {
|
||||
data: DashboardtypesGettableDashboardV2DTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetFeatures200 = {
|
||||
/**
|
||||
* @type array
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import type { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { getNavigationReferrer } from 'lib/navigation';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { isCustomTimeRange } from 'store/globalTime';
|
||||
|
||||
export enum Events {
|
||||
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
|
||||
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
|
||||
@@ -39,3 +45,155 @@ export enum InfraMonitoringEvents {
|
||||
StatefulSet = 'statefulSet',
|
||||
Volumes = 'volumes',
|
||||
}
|
||||
|
||||
export function logInfraFilterCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
source: 'quick_filter' | 'search' | 'host_status_toggle',
|
||||
expression: string,
|
||||
extraKeys?: string[],
|
||||
): void {
|
||||
const expressionKeys = extractQueryPairs(expression?.trim() || '').map(
|
||||
(pair) => pair.key,
|
||||
);
|
||||
|
||||
if (extraKeys) {
|
||||
extraKeys.forEach((key) => expressionKeys.push(key));
|
||||
}
|
||||
|
||||
if (expressionKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void logEvent('infra_filter_customized', {
|
||||
entity_type: entityType,
|
||||
source,
|
||||
expression_keys: [...new Set(expressionKeys)],
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraMonitoringListViewedEvent(
|
||||
entity: InfraMonitoringEntity,
|
||||
): void {
|
||||
const referrer = getNavigationReferrer();
|
||||
|
||||
void logEvent('infra_list_viewed', {
|
||||
entity,
|
||||
referrer,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraTimeRangeCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
rangeLabel: string,
|
||||
): void {
|
||||
void logEvent('infra_time_range_customized', {
|
||||
entity_type: entityType,
|
||||
range_label: isCustomTimeRange(rangeLabel) ? 'custom' : rangeLabel,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraColumnCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
columnsList: string[],
|
||||
fontSize: string,
|
||||
maxLinesPerRow: number,
|
||||
source: 'list' | 'expanded',
|
||||
): void {
|
||||
void logEvent('infra_column_customized', {
|
||||
entity_type: entityType,
|
||||
columns_list: columnsList,
|
||||
font_size: fontSize,
|
||||
max_lines_per_row: maxLinesPerRow,
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraColumnSortedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
columnKey: string,
|
||||
direction: 'asc' | 'desc',
|
||||
source: 'list' | 'expanded',
|
||||
): void {
|
||||
void logEvent('infra_column_sorted', {
|
||||
entity_type: entityType,
|
||||
column_key: columnKey,
|
||||
direction,
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraDrawerTimeRangeCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
rangeLabel: string,
|
||||
): void {
|
||||
void logEvent('infra_drawer_time_range_customized', {
|
||||
entity_type: entityType,
|
||||
range_label: isCustomTimeRange(rangeLabel) ? 'custom' : rangeLabel,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraDrawerFilterCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
tab: 'metrics' | 'logs' | 'traces' | 'events' | 'pod_metrics',
|
||||
expression: string,
|
||||
filterSource: 'search' | 'logs',
|
||||
): void {
|
||||
const expressionKeys = extractQueryPairs(expression?.trim() || '').map(
|
||||
(pair) => pair.key,
|
||||
);
|
||||
|
||||
if (expressionKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void logEvent('infra_drawer_filter_customized', {
|
||||
entity_type: entityType,
|
||||
tab,
|
||||
expression_keys: [...new Set(expressionKeys)],
|
||||
filter_source: filterSource,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraGroupByCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
groupByKeysList: string[],
|
||||
): void {
|
||||
void logEvent('infra_group_by_customized', {
|
||||
entity_type: entityType,
|
||||
group_by_keys_list: groupByKeysList,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraDrawerTabViewedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
tab: string,
|
||||
isDefaultTab: boolean,
|
||||
): void {
|
||||
void logEvent('infra_drawer_tab_viewed', {
|
||||
entity_type: entityType,
|
||||
tab,
|
||||
is_default_tab: isDefaultTab,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraExplorerNavigatedEvent(params: {
|
||||
entityType: InfraMonitoringEntity;
|
||||
destination:
|
||||
| 'metrics_explorer'
|
||||
| 'logs_explorer'
|
||||
| 'traces_explorer'
|
||||
| 'k8s_list';
|
||||
source: 'chart_compass_icon' | 'tab_cta_button' | 'stats_card';
|
||||
tab: string;
|
||||
sourceKey: string | null;
|
||||
drawerDurationMsAtNavigation: number | null;
|
||||
}): void {
|
||||
void logEvent('infra_explorer_navigated', {
|
||||
entity_type: params.entityType,
|
||||
destination: params.destination,
|
||||
source: params.source,
|
||||
tab: params.tab,
|
||||
source_key: params.sourceKey,
|
||||
drawer_duration_ms_at_navigation: params.drawerDurationMsAtNavigation,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ export enum FeatureKeys {
|
||||
ANOMALY_DETECTION = 'anomaly_detection',
|
||||
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
|
||||
USE_JSON_BODY = 'use_json_body',
|
||||
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
|
||||
USE_INFRA_MONITORING_V2 = 'use_infra_monitoring_v2',
|
||||
ENABLE_AI_OBSERVABILITY = 'enable_ai_observability',
|
||||
ENABLE_METRICS_REDUCTION = 'enable_metrics_reduction',
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// ** Helpers
|
||||
import {
|
||||
MetrictypesTemporalityDTO,
|
||||
MetrictypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
||||
@@ -392,17 +389,11 @@ const METRIC_TYPE_TO_ATTRIBUTE_TYPE: Record<
|
||||
export function toAttributeType(
|
||||
metricType: MetrictypesTypeDTO | undefined,
|
||||
isMonotonic?: boolean,
|
||||
temporality?: MetrictypesTemporalityDTO,
|
||||
): ATTRIBUTE_TYPES | '' {
|
||||
if (!metricType) {
|
||||
return '';
|
||||
}
|
||||
// Only non-monotonic cumulative sums are treated as gauges; delta sums stay Sum
|
||||
if (
|
||||
metricType === MetrictypesTypeDTO.sum &&
|
||||
isMonotonic === false &&
|
||||
temporality === MetrictypesTemporalityDTO.cumulative
|
||||
) {
|
||||
if (metricType === MetrictypesTypeDTO.sum && isMonotonic === false) {
|
||||
return ATTRIBUTE_TYPES.GAUGE;
|
||||
}
|
||||
return METRIC_TYPE_TO_ATTRIBUTE_TYPE[metricType] || '';
|
||||
|
||||
@@ -17,7 +17,11 @@ import {
|
||||
QuickFilterChangeEventData,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraFilterCustomizedEvent,
|
||||
logInfraMonitoringListViewedEvent,
|
||||
} from 'constants/events';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import K8sBaseDetails, {
|
||||
K8sDetailsFilters,
|
||||
@@ -53,10 +57,6 @@ import styles from './InfraMonitoringHosts.module.scss';
|
||||
import { ArrowUpToLine, Filter } from '@signozhq/icons';
|
||||
import { NANO_SECOND_MULTIPLIER, useGlobalTimeStore } from 'store/globalTime';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import {
|
||||
logInfraFilterCustomizedEvent,
|
||||
logInfraMonitoringListViewedEvent,
|
||||
} from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
function Hosts(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
|
||||
import { logInfraFilterCustomizedEvent } from 'constants/events';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import {
|
||||
StatusFilterValue,
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
import styles from './StatusFilter.module.scss';
|
||||
import { logInfraFilterCustomizedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
const statusOptions: Array<{
|
||||
label: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import {
|
||||
@@ -9,7 +10,6 @@ import { K8sDetailsMetadataConfig } from 'container/InfraMonitoringK8sV2/Base/K8
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { TextNoData } from 'container/InfraMonitoringK8sV2/components';
|
||||
import { getStrokeColorForPercent } from 'container/InfraMonitoringK8sV2/components/EntityProgressBar.utils';
|
||||
import { SelectedItemParams } from 'container/InfraMonitoringK8sV2/hooks';
|
||||
import {
|
||||
getHostQueryPayload,
|
||||
@@ -18,6 +18,26 @@ import {
|
||||
|
||||
import infraHostsStyles from './InfraMonitoringHosts.module.scss';
|
||||
|
||||
export function getProgressColor(percent: number): string {
|
||||
if (percent >= 90) {
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
if (percent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
|
||||
export function getMemoryProgressColor(percent: number): string {
|
||||
if (percent >= 90) {
|
||||
return Color.BG_CHERRY_500;
|
||||
}
|
||||
if (percent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
|
||||
export type HostDetailMetadataConfigType =
|
||||
K8sDetailsMetadataConfig<InframonitoringtypesHostRecordDTO>;
|
||||
export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
|
||||
@@ -59,7 +79,7 @@ export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
|
||||
render: (value): React.ReactNode => (
|
||||
<Progress
|
||||
percent={Number(Number(value).toFixed(1))}
|
||||
strokeColor={getStrokeColorForPercent('cpu', Number(value))}
|
||||
strokeColor={getProgressColor(Number(value))}
|
||||
showInfo
|
||||
/>
|
||||
),
|
||||
@@ -70,7 +90,7 @@ export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
|
||||
render: (value): React.ReactNode => (
|
||||
<Progress
|
||||
percent={Number(Number(value).toFixed(1))}
|
||||
strokeColor={getStrokeColorForPercent('memory', Number(value))}
|
||||
strokeColor={getMemoryProgressColor(Number(value))}
|
||||
showInfo
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -9,7 +9,6 @@ import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { getGroupByEl } from 'container/InfraMonitoringK8sV2/Base/utils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
EntityProgressThresholds,
|
||||
ExpandButtonWrapper,
|
||||
GroupedStatusCounts,
|
||||
ValidateColumnValueWrapper,
|
||||
@@ -99,7 +98,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
),
|
||||
},
|
||||
{
|
||||
id: INFRA_MONITORING_ATTR_KEYS.HOST_NAME,
|
||||
id: 'hostName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Hostname"
|
||||
@@ -109,7 +108,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
),
|
||||
accessorFn: (row): string => row.hostName ?? '',
|
||||
width: { min: 290 },
|
||||
enableSort: true,
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
@@ -169,10 +168,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
{
|
||||
id: 'cpu',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/host-monitoring#cpu-usage"
|
||||
tooltip={<EntityProgressThresholds type="cpu" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#cpu-usage">
|
||||
CPU Usage
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -199,9 +195,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
id: 'memory',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
tooltip={
|
||||
<EntityProgressThresholds type="memory" note="Excluding cache memory." />
|
||||
}
|
||||
tooltip="Excluding cache memory."
|
||||
docPath="/infrastructure-monitoring/host-monitoring#memory-usage"
|
||||
>
|
||||
Memory Usage (WSS)
|
||||
@@ -227,12 +221,9 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'disk_usage',
|
||||
id: 'diskUsage',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/host-monitoring#disk-usage"
|
||||
tooltip={<EntityProgressThresholds type="disk" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#disk-usage">
|
||||
Disk Usage
|
||||
</ColumnHeader>
|
||||
),
|
||||
|
||||
@@ -3,14 +3,13 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
|
||||
import styles from './ColumnHeader.module.scss';
|
||||
import cx from 'classnames';
|
||||
import { MouseEventHandler } from 'react';
|
||||
|
||||
const DOCS_BASE_URL = `${process.env.DOCS_BASE_URL}/docs`;
|
||||
|
||||
interface ColumnHeaderProps {
|
||||
children?: React.ReactNode;
|
||||
docPath?: string;
|
||||
tooltip?: React.ReactNode;
|
||||
tooltip?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -20,9 +19,6 @@ function ColumnHeader({
|
||||
tooltip,
|
||||
className,
|
||||
}: ColumnHeaderProps): JSX.Element {
|
||||
const stopPropagationHandler: MouseEventHandler = (e): void =>
|
||||
e.stopPropagation();
|
||||
|
||||
const renderContent = (): React.ReactNode => {
|
||||
if (children) {
|
||||
return children;
|
||||
@@ -34,25 +30,21 @@ function ColumnHeader({
|
||||
const renderInfoIcon = (): React.ReactNode => {
|
||||
if (docPath) {
|
||||
const tooltipTitle = tooltip || 'Not sure what this means?';
|
||||
const isJustStringTitle = typeof tooltipTitle === 'string';
|
||||
|
||||
return (
|
||||
<TooltipSimple
|
||||
arrow
|
||||
title={
|
||||
<div onClick={stopPropagationHandler}>
|
||||
<>
|
||||
{tooltipTitle}{' '}
|
||||
<a
|
||||
href={`${DOCS_BASE_URL}${docPath}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
onClick={stopPropagationHandler}
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
>
|
||||
{isJustStringTitle
|
||||
? 'Learn more.'
|
||||
: 'Check the documentation to learn more.'}
|
||||
Learn more.
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={styles.infoIcon}>
|
||||
@@ -64,9 +56,7 @@ function ColumnHeader({
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<TooltipSimple
|
||||
title={<div onClick={stopPropagationHandler}>{tooltip}</div>}
|
||||
>
|
||||
<TooltipSimple title={tooltip}>
|
||||
<div className={styles.infoIcon}>
|
||||
<Info size="md" />
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,11 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraDrawerTabViewedEvent,
|
||||
logInfraExplorerNavigatedEvent,
|
||||
} from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
@@ -46,8 +50,6 @@ import { K8sBaseDetailsContentProps } from './types';
|
||||
import { getDrawerDurationMs } from './useDrawerLifecycleStore';
|
||||
|
||||
import styles from '../EntityDetailsUtils/entityDetails.module.scss';
|
||||
import { logInfraDrawerTabViewedEvent } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/events';
|
||||
import { logInfraExplorerNavigatedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function K8sBaseDetailsContent<T>({
|
||||
|
||||
@@ -9,7 +9,11 @@ import TanStackTable, {
|
||||
useHiddenColumnIds,
|
||||
useTableParams,
|
||||
} from 'components/TanStackTableView';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraColumnSortedEvent,
|
||||
logInfraTimeRangeCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
@@ -44,10 +48,6 @@ import { K8sInstrumentationChecksCallout } from './components/K8sInstrumentation
|
||||
|
||||
import styles from './K8sBaseList.module.scss';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
logInfraColumnSortedEvent,
|
||||
logInfraTimeRangeCustomizedEvent,
|
||||
} from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
export type K8sBaseListEmptyStateContext = {
|
||||
isError: boolean;
|
||||
@@ -128,8 +128,6 @@ export function K8sBaseList<
|
||||
|
||||
const { containerRef, calculatedPageSize } = useCalculatedPageSize({
|
||||
rowHeight: 42,
|
||||
headerHeight: 58,
|
||||
paginationHeight: 52,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -438,17 +436,16 @@ export function K8sBaseList<
|
||||
isFetching={isFetching}
|
||||
cancelQuery={cancelQuery}
|
||||
/>
|
||||
|
||||
<K8sInstrumentationChecksCallout entity={entity} />
|
||||
|
||||
<K8sTableToolbar
|
||||
entity={entity}
|
||||
eventCategory={eventCategory}
|
||||
leftFilters={leftFilters}
|
||||
onOpenOptionsDrawer={handleOpenOptionsDrawer}
|
||||
/>
|
||||
|
||||
<div ref={containerRef} className={styles.tableContainer}>
|
||||
<K8sInstrumentationChecksCallout entity={entity} />
|
||||
|
||||
<K8sTableToolbar
|
||||
entity={entity}
|
||||
eventCategory={eventCategory}
|
||||
leftFilters={leftFilters}
|
||||
onOpenOptionsDrawer={handleOpenOptionsDrawer}
|
||||
/>
|
||||
|
||||
{isError && (
|
||||
<Typography>
|
||||
{data?.error?.toString() || 'Something went wrong'}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
--tanstack-table-resize-handle-hover-bg: var(--l1-border);
|
||||
--tanstack-table-row-height: 36px;
|
||||
|
||||
--tanstack-cell-padding-left-override: 26px;
|
||||
--tanstack-cell-padding-left-override: 15px;
|
||||
--tanstack-cell-padding-right-override: 15px;
|
||||
|
||||
& [data-hide-expanded='true'] {
|
||||
|
||||
@@ -10,19 +10,19 @@ import TanStackTable, {
|
||||
TableColumnDef,
|
||||
TanStackTableStateProvider,
|
||||
} from 'components/TanStackTableView';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { CornerDownRight } from '@signozhq/icons';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
|
||||
import {
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
import { logInfraColumnSortedEvent } from 'constants/events';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
SelectedItemParams,
|
||||
useInfraMonitoringGroupBy,
|
||||
@@ -36,9 +36,6 @@ import { useInfraMonitoringFontSize } from './useInfraMonitoringTablePreferences
|
||||
|
||||
import styles from './K8sExpandedRow.module.scss';
|
||||
import { buildExpressionFromGroupMeta } from './utils';
|
||||
import { logInfraColumnSortedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
import { getUnstableCurrentSearchParams } from 'container/TopNav/DateTimeSelectionV2/utils/getUnstableCurrentSearchParams';
|
||||
import { QueryParams } from 'constants/query';
|
||||
|
||||
const EXPANDED_ROW_LIMIT = 10;
|
||||
|
||||
@@ -95,6 +92,7 @@ export function K8sExpandedRow<
|
||||
const [, setSelectedItemParams] = useInfraMonitoringSelectedItemParams();
|
||||
const [, setMainOrderBy] = useInfraMonitoringOrderBy();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const urlQuery = useUrlQuery();
|
||||
const location = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -260,26 +258,13 @@ export function K8sExpandedRow<
|
||||
},
|
||||
};
|
||||
|
||||
const searchParams = getUnstableCurrentSearchParams();
|
||||
|
||||
searchParams.set(
|
||||
const newUrlQuery = new URLSearchParams(urlQuery.toString());
|
||||
newUrlQuery.set(
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(updatedQuery)),
|
||||
);
|
||||
|
||||
searchParams.delete(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||
searchParams.delete(INFRA_MONITORING_K8S_PARAMS_KEYS.EXPANDED);
|
||||
searchParams.delete(orderByParamKey);
|
||||
searchParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE, '1');
|
||||
|
||||
if (orderBy) {
|
||||
searchParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
|
||||
JSON.stringify(orderBy),
|
||||
);
|
||||
}
|
||||
|
||||
safeNavigate(`${location.pathname}?${searchParams.toString()}`);
|
||||
safeNavigate(`${location.pathname}?${newUrlQuery.toString()}`);
|
||||
};
|
||||
|
||||
const total = data?.total ?? 0;
|
||||
@@ -291,7 +276,6 @@ export function K8sExpandedRow<
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
className={styles.viewAllButton}
|
||||
data-testid="expanded-row-view-all"
|
||||
onClick={handleViewAllClick}
|
||||
prefix={<CornerDownRight size={14} />}
|
||||
>
|
||||
|
||||
@@ -2,7 +2,10 @@ import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraFilterCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
@@ -21,7 +24,6 @@ import {
|
||||
import { useInfraMonitoringPageListing } from '../hooks';
|
||||
|
||||
import styles from './K8sHeader.module.scss';
|
||||
import { logInfraFilterCustomizedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
interface K8sHeaderProps {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
|
||||
@@ -4,34 +4,19 @@ import { Select } from 'antd';
|
||||
import { Download, SlidersVertical } from '@signozhq/icons';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
|
||||
import {
|
||||
INFRA_MONITORING_ATTR_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
InfraMonitoringEvents,
|
||||
logInfraGroupByCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringPageListing,
|
||||
} from '../hooks';
|
||||
import { useInfraMonitoringGroupByData } from './useInfraMonitoringGroupByData';
|
||||
|
||||
import styles from './K8sTableToolbar.module.scss';
|
||||
import { logInfraGroupByCustomizedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
const NAME_COLUMN_KEYS: Set<string> = new Set([
|
||||
INFRA_MONITORING_ATTR_KEYS.HOST_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME,
|
||||
]);
|
||||
|
||||
interface K8sTableToolbarProps {
|
||||
entity: InfraMonitoringEntity;
|
||||
@@ -52,17 +37,11 @@ function K8sTableToolbar({
|
||||
useInfraMonitoringGroupByData(entity);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: string[]) => {
|
||||
void setCurrentPage(1);
|
||||
|
||||
if (orderBy && NAME_COLUMN_KEYS.has(orderBy.columnName)) {
|
||||
void setOrderBy(null);
|
||||
}
|
||||
|
||||
void setGroupBy(value);
|
||||
|
||||
void logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
@@ -73,16 +52,15 @@ function K8sTableToolbar({
|
||||
|
||||
logInfraGroupByCustomizedEvent(entity, value);
|
||||
},
|
||||
[entity, eventCategory, orderBy, setCurrentPage, setOrderBy, setGroupBy],
|
||||
[entity, eventCategory, setCurrentPage, setGroupBy],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.toolbar}>
|
||||
<div className={styles.groupByContainer} data-testid="k8s-table-group-by">
|
||||
<div className={styles.groupByContainer}>
|
||||
<div className={styles.groupByLabel}>Group by</div>
|
||||
<Select
|
||||
className={styles.groupBySelect}
|
||||
data-testid="k8s-table-group-by-select"
|
||||
loading={isLoadingGroupByFilters}
|
||||
mode="multiple"
|
||||
value={groupBy}
|
||||
|
||||
@@ -1370,127 +1370,4 @@ describe('K8sBaseList', () => {
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupBy change clears orderBy', () => {
|
||||
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
|
||||
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
|
||||
>();
|
||||
|
||||
beforeEach(() => {
|
||||
onUrlUpdateMock.mockClear();
|
||||
fetchListDataMock.mockClear();
|
||||
fetchListDataMock.mockResolvedValue({
|
||||
data: [{ id: 'item-1' }],
|
||||
total: 1,
|
||||
error: null,
|
||||
});
|
||||
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
|
||||
res(ctx.json({ status: 'success', data: { ready: true } })),
|
||||
),
|
||||
rest.get('http://localhost/api/v1/fields/keys', (_, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
keys: {
|
||||
resource: [{ name: 'k8s.namespace.name' }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear orderBy for name columns when groupBy is changed', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderComponent<TestItem>({
|
||||
onUrlUpdate: onUrlUpdateMock,
|
||||
entity: InfraMonitoringEntity.PODS,
|
||||
eventCategory: InfraMonitoringEvents.Pod,
|
||||
fetchListData: fetchListDataMock,
|
||||
queryParams: {
|
||||
// k8s.pod.name is a name column - should be cleared
|
||||
orderBy: JSON.stringify({ columnName: 'k8s.pod.name', order: 'desc' }),
|
||||
},
|
||||
tableColumns: createTestColumns(),
|
||||
getRowKey: (row): string => row.id,
|
||||
getItemKey: (row): string => row.id,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('k8s-table-group-by')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Open group by dropdown using testId
|
||||
const groupByContainer = screen.getByTestId('k8s-table-group-by-select');
|
||||
const groupBySelect = groupByContainer.querySelector(
|
||||
'.ant-select-selector',
|
||||
) as Element;
|
||||
await user.click(groupBySelect);
|
||||
|
||||
// Wait for options to load and click on the namespace option
|
||||
const namespaceOption = await screen.findByTitle('k8s.namespace.name');
|
||||
await user.click(namespaceOption);
|
||||
|
||||
// Verify orderBy was cleared (set to null) for name column
|
||||
await waitFor(() => {
|
||||
const orderByCalls = onUrlUpdateMock.mock.calls
|
||||
.map((call) => call[0].searchParams.get('orderBy'))
|
||||
.filter((v) => v !== undefined);
|
||||
|
||||
const hasOrderByCleared = orderByCalls.some((v) => v === null);
|
||||
expect(hasOrderByCleared).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep orderBy for non-name columns when groupBy is changed', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderComponent<TestItem>({
|
||||
onUrlUpdate: onUrlUpdateMock,
|
||||
entity: InfraMonitoringEntity.PODS,
|
||||
eventCategory: InfraMonitoringEvents.Pod,
|
||||
fetchListData: fetchListDataMock,
|
||||
queryParams: {
|
||||
// cpu is NOT a name column - should be kept
|
||||
orderBy: JSON.stringify({ columnName: 'cpu', order: 'desc' }),
|
||||
},
|
||||
tableColumns: createTestColumns(),
|
||||
getRowKey: (row): string => row.id,
|
||||
getItemKey: (row): string => row.id,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('k8s-table-group-by')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Open group by dropdown using testId
|
||||
const groupByContainer = screen.getByTestId('k8s-table-group-by-select');
|
||||
const groupBySelect = groupByContainer.querySelector(
|
||||
'.ant-select-selector',
|
||||
) as Element;
|
||||
await user.click(groupBySelect);
|
||||
|
||||
// Wait for options to load and click on the namespace option
|
||||
const namespaceOption = await screen.findByTitle('k8s.namespace.name');
|
||||
await user.click(namespaceOption);
|
||||
|
||||
// Verify orderBy was NOT cleared for non-name column
|
||||
await waitFor(() => {
|
||||
const orderByCalls = onUrlUpdateMock.mock.calls
|
||||
.map((call) => call[0].searchParams.get('orderBy'))
|
||||
.filter((v) => v !== undefined);
|
||||
|
||||
// orderBy should never be set to null
|
||||
const hasOrderByCleared = orderByCalls.some((v) => v === null);
|
||||
expect(hasOrderByCleared).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { TableColumnDef, useColumnStore } from 'components/TanStackTableView';
|
||||
import { logInfraColumnCustomizedEvent } from 'constants/events';
|
||||
|
||||
import { InfraMonitoringEntity } from '../../constants';
|
||||
import { useInfraMonitoringTablePreferencesStore } from '../useInfraMonitoringTablePreferencesStore';
|
||||
import { useLogEventForColumnCustomized } from '../useLogEventForColumnCustomized';
|
||||
import { logInfraColumnCustomizedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
jest.mock('container/InfraMonitoringK8sV2/Base/events', () => ({
|
||||
jest.mock('constants/events', () => ({
|
||||
logInfraColumnCustomizedEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Compass } from '@signozhq/icons';
|
||||
import { TextNoData } from '../../../components/TextNoData';
|
||||
import { logInfraExplorerNavigatedEvent } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
} from '../../../constants';
|
||||
import { getDrawerDurationMs } from '../../useDrawerLifecycleStore';
|
||||
import styles from './EntityCountsSection.module.scss';
|
||||
import { logInfraExplorerNavigatedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
export interface EntityCountConfig<T> {
|
||||
label: string;
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import type { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getNavigationReferrer } from 'lib/navigation';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { isCustomTimeRange } from 'store/globalTime';
|
||||
|
||||
export function logInfraFilterCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
source: 'quick_filter' | 'search' | 'host_status_toggle',
|
||||
expression: string,
|
||||
extraKeys?: string[],
|
||||
): void {
|
||||
const expressionKeys = extractQueryPairs(expression?.trim() || '').map(
|
||||
(pair) => pair.key,
|
||||
);
|
||||
|
||||
if (extraKeys) {
|
||||
extraKeys.forEach((key) => expressionKeys.push(key));
|
||||
}
|
||||
|
||||
if (expressionKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void logEvent('infra_filter_customized', {
|
||||
entity_type: entityType,
|
||||
source,
|
||||
expression_keys: [...new Set(expressionKeys)],
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraMonitoringListViewedEvent(
|
||||
entity: InfraMonitoringEntity,
|
||||
): void {
|
||||
const referrer = getNavigationReferrer();
|
||||
|
||||
void logEvent('infra_list_viewed', {
|
||||
entity,
|
||||
referrer,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraTimeRangeCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
rangeLabel: string,
|
||||
): void {
|
||||
void logEvent('infra_time_range_customized', {
|
||||
entity_type: entityType,
|
||||
range_label: isCustomTimeRange(rangeLabel) ? 'custom' : rangeLabel,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraColumnCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
columnsList: string[],
|
||||
fontSize: string,
|
||||
maxLinesPerRow: number,
|
||||
source: 'list' | 'expanded',
|
||||
): void {
|
||||
void logEvent('infra_column_customized', {
|
||||
entity_type: entityType,
|
||||
columns_list: columnsList,
|
||||
font_size: fontSize,
|
||||
max_lines_per_row: maxLinesPerRow,
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraColumnSortedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
columnKey: string,
|
||||
direction: 'asc' | 'desc',
|
||||
source: 'list' | 'expanded',
|
||||
): void {
|
||||
void logEvent('infra_column_sorted', {
|
||||
entity_type: entityType,
|
||||
column_key: columnKey,
|
||||
direction,
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraGroupByCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
groupByKeysList: string[],
|
||||
): void {
|
||||
void logEvent('infra_group_by_customized', {
|
||||
entity_type: entityType,
|
||||
group_by_keys_list: groupByKeysList,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraExplorerNavigatedEvent(params: {
|
||||
entityType: InfraMonitoringEntity;
|
||||
destination:
|
||||
| 'metrics_explorer'
|
||||
| 'logs_explorer'
|
||||
| 'traces_explorer'
|
||||
| 'k8s_list';
|
||||
source: 'chart_compass_icon' | 'tab_cta_button' | 'stats_card';
|
||||
tab: string;
|
||||
sourceKey: string | null;
|
||||
drawerDurationMsAtNavigation: number | null;
|
||||
}): void {
|
||||
void logEvent('infra_explorer_navigated', {
|
||||
entity_type: params.entityType,
|
||||
destination: params.destination,
|
||||
source: params.source,
|
||||
tab: params.tab,
|
||||
source_key: params.sourceKey,
|
||||
drawer_duration_ms_at_navigation: params.drawerDurationMsAtNavigation,
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
useColumnOrder,
|
||||
useHiddenColumnIds,
|
||||
} from 'components/TanStackTableView';
|
||||
import { logInfraColumnCustomizedEvent } from 'constants/events';
|
||||
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
useInfraMonitoringLineClamp,
|
||||
} from './useInfraMonitoringTablePreferencesStore';
|
||||
import { sortByColumnOrder } from './utils';
|
||||
import { logInfraColumnCustomizedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
interface UseEmitColumnCustomizedParams<TData> {
|
||||
entity: InfraMonitoringEntity;
|
||||
|
||||
@@ -60,7 +60,7 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME,
|
||||
id: 'clusterName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Cluster Name"
|
||||
@@ -70,7 +70,7 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
|
||||
),
|
||||
accessorFn: (row): string => row.clusterName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: true,
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
|
||||
@@ -10,7 +10,6 @@ import { SelectedItemParams } from '../hooks';
|
||||
import { formatBytes, getPodStatusItems } from '../commonUtils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
EntityProgressThresholds,
|
||||
GroupedStatusCounts,
|
||||
TextNoData,
|
||||
ValidateColumnValueWrapper,
|
||||
@@ -70,7 +69,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME,
|
||||
id: 'daemonsetName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="DaemonSet Name"
|
||||
@@ -81,7 +80,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: true,
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
@@ -175,10 +174,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-request" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-req-usage-">
|
||||
CPU Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -196,7 +192,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="cpu-request" />
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -204,10 +200,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-limit" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-limit-usage-">
|
||||
CPU Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -224,7 +217,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -258,10 +251,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-request" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-req-usage-">
|
||||
Memory Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -279,7 +269,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="memory-request" />
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -287,10 +277,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-limit" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-limit-usage-">
|
||||
Memory Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -307,7 +294,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="memory-limit" />
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -10,7 +10,6 @@ import { SelectedItemParams } from '../hooks';
|
||||
import { formatBytes, getPodStatusItems } from '../commonUtils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
EntityProgressThresholds,
|
||||
GroupedStatusCounts,
|
||||
TextNoData,
|
||||
ValidateColumnValueWrapper,
|
||||
@@ -71,7 +70,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
},
|
||||
},
|
||||
{
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
id: 'deploymentName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Deployment Name"
|
||||
@@ -82,7 +81,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: true,
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
@@ -163,10 +162,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-request" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-req-usage-">
|
||||
CPU Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -184,7 +180,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="cpu-request" />
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -192,10 +188,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-limit" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-limit-usage-">
|
||||
CPU Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -212,7 +205,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -245,10 +238,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/deployments#mem-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-request" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-req-usage-">
|
||||
Memory Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -266,7 +256,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="memory-request" />
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -274,10 +264,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/deployments#mem-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-limit" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-limit-usage-">
|
||||
Memory Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -294,7 +281,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="memory-limit" />
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -3,7 +3,10 @@ import { Undo } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraDrawerTimeRangeCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
@@ -14,7 +17,6 @@ import {
|
||||
import { useEntityDetailsTime } from './useEntityDetailsTime';
|
||||
|
||||
import styles from './EntityDateTimeSelector.module.scss';
|
||||
import { logInfraDrawerTimeRangeCustomizedEvent } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/events';
|
||||
|
||||
interface EntityDateTimeSelectorProps {
|
||||
eventEntity: string;
|
||||
|
||||
@@ -16,7 +16,10 @@ import {
|
||||
combineInitialAndUserExpression,
|
||||
getUserExpressionFromCombined,
|
||||
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraDrawerFilterCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import Controls from 'container/Controls';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import LoadingContainer from 'container/InfraMonitoringK8sV2/LoadingContainer';
|
||||
@@ -38,7 +41,6 @@ import { getEntityEventsQueryPayload, isEventsKeyNotFoundError } from './utils';
|
||||
|
||||
import styles from './EntityEvents.module.scss';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { logInfraDrawerFilterCustomizedEvent } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/events';
|
||||
|
||||
interface EventDataType {
|
||||
key: string;
|
||||
|
||||
@@ -20,7 +20,10 @@ import {
|
||||
combineInitialAndUserExpression,
|
||||
getUserExpressionFromCombined,
|
||||
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraDrawerFilterCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
@@ -50,7 +53,6 @@ import { isModifierKeyPressed } from 'utils/app';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { logInfraDrawerFilterCustomizedEvent } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/events';
|
||||
|
||||
interface Props {
|
||||
eventEntity: string;
|
||||
|
||||
@@ -2,7 +2,10 @@ import { useCallback, useMemo, useRef } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Skeleton } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraExplorerNavigatedEvent,
|
||||
} from 'constants/events';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
@@ -32,7 +35,6 @@ import { isKeyNotFoundError } from '../utils';
|
||||
|
||||
import styles from './EntityMetrics.module.scss';
|
||||
import { MetricsTable } from './MetricsTable';
|
||||
import { logInfraExplorerNavigatedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
|
||||
interface EntityMetricsProps<T> {
|
||||
entity: T;
|
||||
|
||||
@@ -16,7 +16,10 @@ import {
|
||||
getUserExpressionFromCombined,
|
||||
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraDrawerFilterCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import Controls from 'container/Controls';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
@@ -38,7 +41,6 @@ import { getEntityTracesQueryPayload } from './utils';
|
||||
|
||||
import styles from './EntityTraces.module.scss';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { logInfraDrawerFilterCustomizedEvent } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/events';
|
||||
|
||||
interface Props {
|
||||
eventEntity: string;
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { isCustomTimeRange } from 'store/globalTime';
|
||||
|
||||
export function logInfraDrawerTimeRangeCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
rangeLabel: string,
|
||||
): void {
|
||||
void logEvent('infra_drawer_time_range_customized', {
|
||||
entity_type: entityType,
|
||||
range_label: isCustomTimeRange(rangeLabel) ? 'custom' : rangeLabel,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraDrawerFilterCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
tab: 'metrics' | 'logs' | 'traces' | 'events' | 'pod_metrics',
|
||||
expression: string,
|
||||
filterSource: 'search' | 'logs',
|
||||
): void {
|
||||
const expressionKeys = extractQueryPairs(expression?.trim() || '').map(
|
||||
(pair) => pair.key,
|
||||
);
|
||||
|
||||
if (expressionKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void logEvent('infra_drawer_filter_customized', {
|
||||
entity_type: entityType,
|
||||
tab,
|
||||
expression_keys: [...new Set(expressionKeys)],
|
||||
filter_source: filterSource,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraDrawerTabViewedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
tab: string,
|
||||
isDefaultTab: boolean,
|
||||
): void {
|
||||
void logEvent('infra_drawer_tab_viewed', {
|
||||
entity_type: entityType,
|
||||
tab,
|
||||
is_default_tab: isDefaultTab,
|
||||
});
|
||||
}
|
||||
@@ -51,14 +51,14 @@ import {
|
||||
} from './hooks';
|
||||
|
||||
import styles from './InfraMonitoringK8s.module.scss';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { NANO_SECOND_MULTIPLIER, useGlobalTimeStore } from 'store/globalTime';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import {
|
||||
logInfraFilterCustomizedEvent,
|
||||
logInfraMonitoringListViewedEvent,
|
||||
} from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
InfraMonitoringEvents,
|
||||
} from 'constants/events';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { NANO_SECOND_MULTIPLIER, useGlobalTimeStore } from 'store/globalTime';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
|
||||
export default function InfraMonitoringK8s(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
|
||||
@@ -10,7 +10,6 @@ import { SelectedItemParams } from '../hooks';
|
||||
import { formatBytes, getPodStatusItems } from '../commonUtils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
EntityProgressThresholds,
|
||||
GroupedStatusCounts,
|
||||
TextNoData,
|
||||
ValidateColumnValueWrapper,
|
||||
@@ -64,7 +63,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME,
|
||||
id: 'jobName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Job Name"
|
||||
@@ -75,7 +74,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: true,
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
@@ -159,10 +158,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-request" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-req-usage-">
|
||||
CPU Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -180,7 +176,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
attribute="CPU Request"
|
||||
rowId={rowId}
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="cpu-request" />
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -188,10 +184,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-limit" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-limit-usage-">
|
||||
CPU Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -208,7 +201,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
attribute="CPU Limit"
|
||||
rowId={rowId}
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -241,10 +234,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/jobs#mem-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-request" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-req-usage-">
|
||||
Memory Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -262,7 +252,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
attribute="Memory Request"
|
||||
rowId={rowId}
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="memory-request" />
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -270,10 +260,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/jobs#mem-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-limit" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-limit-usage-">
|
||||
Memory Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -290,7 +277,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
attribute="Memory Limit"
|
||||
rowId={rowId}
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="memory-limit" />
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -111,8 +111,7 @@ export const namespaceWidgetInfo = [
|
||||
{
|
||||
title: 'CPU Usage (cores)',
|
||||
yAxisUnit: '',
|
||||
docPath:
|
||||
'/infrastructure-monitoring/kubernetes/namespaces/#cpu-usage-cores-1',
|
||||
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#cpu-usage-cores',
|
||||
},
|
||||
{
|
||||
title: 'Memory Usage (bytes)',
|
||||
|
||||
@@ -66,7 +66,7 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME,
|
||||
id: 'namespaceName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Namespace Name"
|
||||
@@ -76,7 +76,7 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
|
||||
),
|
||||
accessorFn: (row): string => row.namespaceName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: true,
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
|
||||
@@ -57,7 +57,7 @@ export const nodeWidgetInfo = [
|
||||
{
|
||||
title: 'CPU Usage (cores)',
|
||||
yAxisUnit: '',
|
||||
docPath: '/infrastructure-monitoring/kubernetes/nodes/#cpu-usage-cores-1',
|
||||
docPath: '/infrastructure-monitoring/kubernetes/nodes/#cpu-usage-cores',
|
||||
},
|
||||
{
|
||||
title: 'Memory Usage (bytes)',
|
||||
|
||||
@@ -68,7 +68,7 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME,
|
||||
id: 'nodeName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Node Name"
|
||||
@@ -78,7 +78,7 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
|
||||
),
|
||||
accessorFn: (row): string => row.nodeName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: true,
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
|
||||
@@ -67,7 +67,7 @@ export const podWidgetInfo = [
|
||||
{
|
||||
title: 'CPU Usage (cores)',
|
||||
yAxisUnit: '',
|
||||
docPath: '/infrastructure-monitoring/kubernetes/pods/#cpu-usage-cores-1',
|
||||
docPath: '/infrastructure-monitoring/kubernetes/pods/#cpu-usage-cores',
|
||||
},
|
||||
{
|
||||
title: 'CPU Request, Limit Utilization',
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
} from '../commonUtils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
EntityProgressThresholds,
|
||||
GroupedStatusCounts,
|
||||
TextNoData,
|
||||
ValidateColumnValueWrapper,
|
||||
@@ -69,7 +68,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME,
|
||||
id: 'podName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Pod Name"
|
||||
@@ -80,7 +79,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: true,
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
@@ -97,7 +96,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): string => row.podStatus,
|
||||
width: { min: 250 },
|
||||
width: { min: 160 },
|
||||
enableSort: false,
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
@@ -176,7 +175,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
),
|
||||
accessorFn: (row): number => row.podRestarts,
|
||||
width: { min: 140 },
|
||||
enableSort: false,
|
||||
enableSort: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
const restarts = value as number;
|
||||
return (
|
||||
@@ -194,10 +193,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/pods#cpu-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-request" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-req-usage-">
|
||||
CPU Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -214,7 +210,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="cpu-request" />
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -222,10 +218,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/pods#cpu-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-limit" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-limit-usage-">
|
||||
CPU Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -241,7 +234,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -273,10 +266,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/pods#mem-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-request" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-req-usage-">
|
||||
Memory Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -293,7 +283,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="memory-request" />
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -301,10 +291,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/pods#mem-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-limit" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-limit-usage-">
|
||||
Memory Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -320,7 +307,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="memory-limit" />
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -10,7 +10,6 @@ import { SelectedItemParams } from '../hooks';
|
||||
import { formatBytes, getPodStatusItems } from '../commonUtils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
EntityProgressThresholds,
|
||||
GroupedStatusCounts,
|
||||
TextNoData,
|
||||
ValidateColumnValueWrapper,
|
||||
@@ -71,7 +70,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
},
|
||||
},
|
||||
{
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME,
|
||||
id: 'statefulsetName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="StatefulSet Name"
|
||||
@@ -82,7 +81,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: true,
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
@@ -166,10 +165,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-request" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-req-usage-">
|
||||
CPU Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -187,7 +183,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="cpu-request" />
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -195,10 +191,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="cpu-limit" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-limit-usage-">
|
||||
CPU Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -215,7 +208,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -249,10 +242,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-req-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-request" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-req-usage-">
|
||||
Memory Request Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -270,7 +260,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="memory-request" />
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -278,10 +268,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-limit-usage-"
|
||||
tooltip={<EntityProgressThresholds type="memory-limit" />}
|
||||
>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-limit-usage-">
|
||||
Memory Limit Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
@@ -298,7 +285,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="memory-limit" />
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -64,7 +64,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME,
|
||||
id: 'pvcName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="PVC Name"
|
||||
@@ -74,7 +74,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
|
||||
),
|
||||
accessorFn: (row): string => row.persistentVolumeClaimName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: true,
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
@@ -195,7 +195,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'inodes_used',
|
||||
id: 'inodesUsed',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes-used">
|
||||
Inodes Used
|
||||
@@ -219,7 +219,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'inodes_free',
|
||||
id: 'inodesFree',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes-free">
|
||||
Inodes Free
|
||||
|
||||
@@ -26,6 +26,48 @@ export function formatBytes(bytes: number, decimals = 2): string {
|
||||
return `${parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stroke color for request utilization parameters according to current value
|
||||
*/
|
||||
export function getStrokeColorForRequestUtilization(value: number): string {
|
||||
const percent = Number((value * 100).toFixed(1));
|
||||
// Orange
|
||||
if (percent <= 50) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
// Green
|
||||
if (percent > 50 && percent <= 100) {
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
// Regular Red
|
||||
if (percent > 100 && percent <= 150) {
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
// Dark Red
|
||||
return Color.BG_CHERRY_600;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stroke color for limit utilization parameters according to current value
|
||||
*/
|
||||
export function getStrokeColorForLimitUtilization(value: number): string {
|
||||
const percent = Number((value * 100).toFixed(1));
|
||||
// Green
|
||||
if (percent <= 60) {
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
// Yellow
|
||||
if (percent > 60 && percent <= 80) {
|
||||
return Color.BG_AMBER_200;
|
||||
}
|
||||
// Orange
|
||||
if (percent > 80 && percent <= 95) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
// Red
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
|
||||
export const POD_STATUS_COLORS: Record<
|
||||
InframonitoringtypesPodStatusDTO,
|
||||
BadgeColor
|
||||
|
||||
@@ -1,11 +1,35 @@
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import {
|
||||
getMemoryProgressColor,
|
||||
getProgressColor,
|
||||
} from 'container/InfraMonitoringHostsV2/constants';
|
||||
|
||||
import {
|
||||
getStrokeColorForLimitUtilization,
|
||||
getStrokeColorForRequestUtilization,
|
||||
} from '../commonUtils';
|
||||
|
||||
import styles from './EntityProgressBar.module.scss';
|
||||
import {
|
||||
EntityProgressBarType,
|
||||
getStrokeColor,
|
||||
} from './EntityProgressBar.utils';
|
||||
|
||||
type EntityProgressBarType = 'request' | 'limit' | 'cpu' | 'memory' | 'disk';
|
||||
|
||||
function getStrokeColor(type: EntityProgressBarType, value: number): string {
|
||||
switch (type) {
|
||||
case 'limit':
|
||||
return getStrokeColorForLimitUtilization(value);
|
||||
case 'request':
|
||||
return getStrokeColorForRequestUtilization(value);
|
||||
case 'cpu':
|
||||
return getProgressColor(Number((value * 100).toFixed(1)));
|
||||
case 'memory':
|
||||
return getMemoryProgressColor(Number((value * 100).toFixed(1)));
|
||||
case 'disk':
|
||||
return getProgressColor(Number((value * 100).toFixed(1)));
|
||||
default:
|
||||
return getStrokeColorForRequestUtilization(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function EntityProgressBar({
|
||||
value,
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
|
||||
export type EntityProgressBarType =
|
||||
| 'cpu-request'
|
||||
| 'cpu-limit'
|
||||
| 'memory-request'
|
||||
| 'memory-limit'
|
||||
| 'cpu'
|
||||
| 'memory'
|
||||
| 'disk';
|
||||
|
||||
export interface EntityProgressThreshold {
|
||||
matches: (percent: number) => boolean;
|
||||
color: string;
|
||||
range: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const CPU_REQUEST_THRESHOLDS: EntityProgressThreshold[] = [
|
||||
{
|
||||
matches: (percent): boolean => percent <= 50,
|
||||
color: Color.BG_AMBER_500,
|
||||
range: '≤ 50%',
|
||||
label: 'Over-requested',
|
||||
description:
|
||||
'CPU usage is at most half of the request. The rest of the request stays reserved on the node.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 100,
|
||||
color: Color.BG_FOREST_500,
|
||||
range: '> 50% - 100%',
|
||||
label: 'Right-sized',
|
||||
description: 'CPU usage is close to the request and stays within it.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 150,
|
||||
color: Color.BG_SAKURA_500,
|
||||
range: '> 100% - 150%',
|
||||
label: 'Over request',
|
||||
description:
|
||||
'CPU usage is above the request. The extra CPU is not guaranteed and depends on spare node capacity.',
|
||||
},
|
||||
{
|
||||
matches: (): boolean => true,
|
||||
color: Color.BG_CHERRY_600,
|
||||
range: '> 150%',
|
||||
label: 'Request badly undersized',
|
||||
description:
|
||||
'CPU usage is more than 1.5x the request, so most of the CPU in use is not guaranteed.',
|
||||
},
|
||||
];
|
||||
|
||||
const CPU_LIMIT_THRESHOLDS: EntityProgressThreshold[] = [
|
||||
{
|
||||
matches: (percent): boolean => percent <= 60,
|
||||
color: Color.BG_FOREST_500,
|
||||
range: '≤ 60%',
|
||||
label: 'Healthy',
|
||||
description: 'CPU usage is well below the limit.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 80,
|
||||
color: Color.BG_AMBER_200,
|
||||
range: '> 60% - 80%',
|
||||
label: 'Watch',
|
||||
description: 'CPU usage is approaching the limit.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 95,
|
||||
color: Color.BG_AMBER_500,
|
||||
range: '> 80% - 95%',
|
||||
label: 'Near limit',
|
||||
description:
|
||||
'CPU usage is close to the limit. Usage above the limit is throttled.',
|
||||
},
|
||||
{
|
||||
matches: (): boolean => true,
|
||||
color: Color.BG_SAKURA_500,
|
||||
range: '> 95%',
|
||||
label: 'At limit',
|
||||
description:
|
||||
'CPU usage is at the limit, so the container is likely being throttled.',
|
||||
},
|
||||
];
|
||||
|
||||
const MEMORY_REQUEST_THRESHOLDS: EntityProgressThreshold[] = [
|
||||
{
|
||||
matches: (percent): boolean => percent <= 50,
|
||||
color: Color.BG_AMBER_500,
|
||||
range: '≤ 50%',
|
||||
label: 'Over-requested',
|
||||
description:
|
||||
'Memory usage is at most half of the request. The rest of the request stays reserved on the node.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 100,
|
||||
color: Color.BG_FOREST_500,
|
||||
range: '> 50% - 100%',
|
||||
label: 'Right-sized',
|
||||
description: 'Memory usage is close to the request and stays within it.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 150,
|
||||
color: Color.BG_SAKURA_500,
|
||||
range: '> 100% - 150%',
|
||||
label: 'Over request',
|
||||
description:
|
||||
'Memory usage is above the request. The extra memory is not guaranteed and is reclaimed first under node memory pressure.',
|
||||
},
|
||||
{
|
||||
matches: (): boolean => true,
|
||||
color: Color.BG_CHERRY_600,
|
||||
range: '> 150%',
|
||||
label: 'Request badly undersized',
|
||||
description:
|
||||
'Memory usage is more than 1.5x the request, so most of the memory in use is not guaranteed.',
|
||||
},
|
||||
];
|
||||
|
||||
const MEMORY_LIMIT_THRESHOLDS: EntityProgressThreshold[] = [
|
||||
{
|
||||
matches: (percent): boolean => percent <= 60,
|
||||
color: Color.BG_FOREST_500,
|
||||
range: '≤ 60%',
|
||||
label: 'Healthy',
|
||||
description: 'Memory usage is well below the limit.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 80,
|
||||
color: Color.BG_AMBER_200,
|
||||
range: '> 60% - 80%',
|
||||
label: 'Watch',
|
||||
description: 'Memory usage is approaching the limit.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent <= 95,
|
||||
color: Color.BG_AMBER_500,
|
||||
range: '> 80% - 95%',
|
||||
label: 'Near limit',
|
||||
description:
|
||||
'Memory usage is close to the limit. Unlike CPU, memory is not throttled: reaching the limit ends in an OOM kill.',
|
||||
},
|
||||
{
|
||||
matches: (): boolean => true,
|
||||
color: Color.BG_SAKURA_500,
|
||||
range: '> 95%',
|
||||
label: 'At limit',
|
||||
description:
|
||||
'Memory usage is at the limit, so an OOM kill and container restart are likely.',
|
||||
},
|
||||
];
|
||||
|
||||
const CPU_THRESHOLDS: EntityProgressThreshold[] = [
|
||||
{
|
||||
matches: (percent): boolean => percent < 60,
|
||||
color: Color.BG_FOREST_500,
|
||||
range: '< 60%',
|
||||
label: 'Healthy',
|
||||
description: 'CPU usage is well below the available capacity.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent < 90,
|
||||
color: Color.BG_AMBER_500,
|
||||
range: '60% - 89.9%',
|
||||
label: 'Elevated',
|
||||
description: 'CPU usage is high relative to the available capacity.',
|
||||
},
|
||||
{
|
||||
matches: (): boolean => true,
|
||||
color: Color.BG_SAKURA_500,
|
||||
range: '≥ 90%',
|
||||
label: 'Critical',
|
||||
description: 'CPU usage is close to the available capacity.',
|
||||
},
|
||||
];
|
||||
|
||||
const MEMORY_THRESHOLDS: EntityProgressThreshold[] = [
|
||||
{
|
||||
matches: (percent): boolean => percent < 60,
|
||||
color: Color.BG_FOREST_500,
|
||||
range: '< 60%',
|
||||
label: 'Healthy',
|
||||
description: 'Memory usage is well below the available capacity.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent < 90,
|
||||
color: Color.BG_AMBER_500,
|
||||
range: '60% - 89.9%',
|
||||
label: 'Elevated',
|
||||
description: 'Memory usage is high relative to the available capacity.',
|
||||
},
|
||||
{
|
||||
matches: (): boolean => true,
|
||||
color: Color.BG_CHERRY_500,
|
||||
range: '≥ 90%',
|
||||
label: 'Critical',
|
||||
description:
|
||||
'Memory usage is close to the available capacity. Unlike CPU, memory is not throttled: running out ends in an OOM kill.',
|
||||
},
|
||||
];
|
||||
|
||||
const DISK_THRESHOLDS: EntityProgressThreshold[] = [
|
||||
{
|
||||
matches: (percent): boolean => percent < 60,
|
||||
color: Color.BG_FOREST_500,
|
||||
range: '< 60%',
|
||||
label: 'Healthy',
|
||||
description: 'Most of the volume is still free.',
|
||||
},
|
||||
{
|
||||
matches: (percent): boolean => percent < 90,
|
||||
color: Color.BG_AMBER_500,
|
||||
range: '60% - 89.9%',
|
||||
label: 'Elevated',
|
||||
description: 'Used space is high relative to the volume capacity.',
|
||||
},
|
||||
{
|
||||
matches: (): boolean => true,
|
||||
color: Color.BG_SAKURA_500,
|
||||
range: '≥ 90%',
|
||||
label: 'Critical',
|
||||
description: 'The volume is nearly full. Writes fail once no space is left.',
|
||||
},
|
||||
];
|
||||
|
||||
export const THRESHOLDS_BY_TYPE: Record<
|
||||
EntityProgressBarType,
|
||||
EntityProgressThreshold[]
|
||||
> = {
|
||||
'cpu-request': CPU_REQUEST_THRESHOLDS,
|
||||
'cpu-limit': CPU_LIMIT_THRESHOLDS,
|
||||
'memory-request': MEMORY_REQUEST_THRESHOLDS,
|
||||
'memory-limit': MEMORY_LIMIT_THRESHOLDS,
|
||||
cpu: CPU_THRESHOLDS,
|
||||
memory: MEMORY_THRESHOLDS,
|
||||
disk: DISK_THRESHOLDS,
|
||||
};
|
||||
|
||||
export function getStrokeColorForPercent(
|
||||
type: EntityProgressBarType,
|
||||
percent: number,
|
||||
): string {
|
||||
const thresholds = THRESHOLDS_BY_TYPE[type];
|
||||
const match = thresholds.find((threshold) => threshold.matches(percent));
|
||||
return (match ?? thresholds[thresholds.length - 1]).color;
|
||||
}
|
||||
|
||||
export function getStrokeColor(
|
||||
type: EntityProgressBarType,
|
||||
value: number,
|
||||
): string {
|
||||
return getStrokeColorForPercent(type, Number((value * 100).toFixed(1)));
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
max-width: 320px;
|
||||
text-align: left;
|
||||
text-wrap: wrap;
|
||||
margin-bottom: var(--spacing-1);
|
||||
}
|
||||
|
||||
.threshold {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.swatch {
|
||||
width: 3px;
|
||||
border-radius: 1px;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--ept-color);
|
||||
}
|
||||
|
||||
.thresholdBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-1);
|
||||
}
|
||||
|
||||
.thresholdHeading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.range {
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import {
|
||||
EntityProgressBarType,
|
||||
THRESHOLDS_BY_TYPE,
|
||||
} from './EntityProgressBar.utils';
|
||||
import styles from './EntityProgressThresholds.module.scss';
|
||||
|
||||
interface EntityProgressThresholdsProps {
|
||||
type: EntityProgressBarType;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export function EntityProgressThresholds({
|
||||
type,
|
||||
note,
|
||||
}: EntityProgressThresholdsProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
data-testid={`entity-progress-thresholds-${type}`}
|
||||
>
|
||||
{note && (
|
||||
<Typography.Text as="p" size="small">
|
||||
{note}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{THRESHOLDS_BY_TYPE[type].map((threshold) => (
|
||||
<div key={threshold.range} className={styles.threshold}>
|
||||
<span
|
||||
className={styles.swatch}
|
||||
style={{ '--ept-color': threshold.color } as React.CSSProperties}
|
||||
/>
|
||||
<div className={styles.thresholdBody}>
|
||||
<div className={styles.thresholdHeading}>
|
||||
<Typography.Text as="span" size="small" weight="medium">
|
||||
{threshold.label}
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
as="span"
|
||||
size="small"
|
||||
color="muted"
|
||||
className={styles.range}
|
||||
>
|
||||
{threshold.range}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text as="p" size="small" color="muted">
|
||||
{threshold.description}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { THRESHOLDS_BY_TYPE } from '../EntityProgressBar.utils';
|
||||
import { EntityProgressThresholds } from '../EntityProgressThresholds';
|
||||
|
||||
describe('EntityProgressThresholds', () => {
|
||||
it('renders every threshold band for the given type', () => {
|
||||
render(<EntityProgressThresholds type="cpu-limit" />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId('entity-progress-thresholds-cpu-limit'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
THRESHOLDS_BY_TYPE['cpu-limit'].forEach((threshold) => {
|
||||
expect(screen.getByText(threshold.label)).toBeInTheDocument();
|
||||
expect(screen.getByText(threshold.range)).toBeInTheDocument();
|
||||
expect(screen.getByText(threshold.description)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the note above the threshold bands when provided', () => {
|
||||
render(
|
||||
<EntityProgressThresholds type="memory" note="Excluding cache memory." />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Excluding cache memory.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
export { EntityProgressBar } from './EntityProgressBar';
|
||||
export { EntityProgressThresholds } from './EntityProgressThresholds';
|
||||
export { ValidateColumnValueWrapper } from './ValidateColumnValueWrapper';
|
||||
export { ExpandButtonWrapper } from './ExpandButtonWrapper';
|
||||
export {
|
||||
|
||||
@@ -33,7 +33,6 @@ function AllAttributes({
|
||||
metricName,
|
||||
metricType,
|
||||
isMonotonic,
|
||||
temporality,
|
||||
minTime,
|
||||
maxTime,
|
||||
}: AllAttributesProps): JSX.Element {
|
||||
@@ -72,7 +71,6 @@ function AllAttributes({
|
||||
groupBy,
|
||||
limit,
|
||||
isMonotonic,
|
||||
temporality,
|
||||
);
|
||||
handleExplorerTabChange(
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
@@ -91,7 +89,7 @@ function AllAttributes({
|
||||
[MetricsExplorerEventKeys.AttributeKey]: groupBy,
|
||||
});
|
||||
},
|
||||
[metricName, metricType, isMonotonic, temporality, handleExplorerTabChange],
|
||||
[metricName, metricType, isMonotonic, handleExplorerTabChange],
|
||||
);
|
||||
|
||||
const goToMetricsExploreWithAppliedAttribute = useCallback(
|
||||
@@ -103,7 +101,6 @@ function AllAttributes({
|
||||
undefined,
|
||||
undefined,
|
||||
isMonotonic,
|
||||
temporality,
|
||||
);
|
||||
handleExplorerTabChange(
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
@@ -123,7 +120,7 @@ function AllAttributes({
|
||||
[MetricsExplorerEventKeys.AttributeValue]: value,
|
||||
});
|
||||
},
|
||||
[metricName, metricType, isMonotonic, temporality, handleExplorerTabChange],
|
||||
[metricName, metricType, isMonotonic, handleExplorerTabChange],
|
||||
);
|
||||
|
||||
const handleKeyMenuItemClick = useCallback(
|
||||
|
||||
@@ -86,7 +86,6 @@ function MetricDetails({
|
||||
undefined,
|
||||
undefined,
|
||||
metadata?.isMonotonic,
|
||||
metadata?.temporality,
|
||||
);
|
||||
handleExplorerTabChange(
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
@@ -109,7 +108,6 @@ function MetricDetails({
|
||||
handleExplorerTabChange,
|
||||
metadata?.type,
|
||||
metadata?.isMonotonic,
|
||||
metadata?.temporality,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -198,7 +196,6 @@ function MetricDetails({
|
||||
metricName={metricName}
|
||||
metricType={metadata?.type}
|
||||
isMonotonic={metadata?.isMonotonic}
|
||||
temporality={metadata?.temporality}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
/>
|
||||
|
||||
@@ -147,44 +147,6 @@ describe('MetricDetails utils', () => {
|
||||
expect(query.builder.queryData[0]?.spaceAggregation).toBe('sum');
|
||||
});
|
||||
|
||||
it('treats a cumulative non-monotonic Sum as a Gauge', () => {
|
||||
const query = getMetricDetailsQuery(
|
||||
TEST_METRIC_NAME,
|
||||
MetrictypesTypeDTO.sum,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
MetrictypesTemporalityDTO.cumulative,
|
||||
);
|
||||
|
||||
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
||||
ATTRIBUTE_TYPES.GAUGE,
|
||||
);
|
||||
expect(query.builder.queryData[0]?.aggregateOperator).toBe('avg');
|
||||
expect(query.builder.queryData[0]?.timeAggregation).toBe('avg');
|
||||
expect(query.builder.queryData[0]?.spaceAggregation).toBe('avg');
|
||||
});
|
||||
|
||||
it('treats a delta non-monotonic Sum as a Sum', () => {
|
||||
const query = getMetricDetailsQuery(
|
||||
TEST_METRIC_NAME,
|
||||
MetrictypesTypeDTO.sum,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
MetrictypesTemporalityDTO.delta,
|
||||
);
|
||||
|
||||
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
||||
ATTRIBUTE_TYPES.SUM,
|
||||
);
|
||||
expect(query.builder.queryData[0]?.aggregateOperator).toBe('rate');
|
||||
expect(query.builder.queryData[0]?.timeAggregation).toBe('rate');
|
||||
expect(query.builder.queryData[0]?.spaceAggregation).toBe('sum');
|
||||
});
|
||||
|
||||
it('should create correct query for GAUGE metric type', () => {
|
||||
const query = getMetricDetailsQuery(
|
||||
TEST_METRIC_NAME,
|
||||
|
||||
@@ -35,7 +35,6 @@ export interface AllAttributesProps {
|
||||
metricName: string;
|
||||
metricType: MetrictypesTypeDTO | undefined;
|
||||
isMonotonic?: boolean;
|
||||
temporality?: MetrictypesTemporalityDTO;
|
||||
minTime?: number;
|
||||
maxTime?: number;
|
||||
}
|
||||
|
||||
@@ -89,16 +89,12 @@ export function getMetricDetailsQuery(
|
||||
groupBy?: string,
|
||||
limit?: number,
|
||||
isMonotonic?: boolean,
|
||||
temporality?: MetrictypesTemporalityDTO,
|
||||
): Query {
|
||||
let timeAggregation;
|
||||
let spaceAggregation;
|
||||
let aggregateOperator;
|
||||
// Only non-monotonic cumulative sums are treated as gauges; delta sums stay Sum
|
||||
const isNonMonotonicSum =
|
||||
metricType === MetrictypesTypeDTO.sum &&
|
||||
isMonotonic === false &&
|
||||
temporality === MetrictypesTemporalityDTO.cumulative;
|
||||
metricType === MetrictypesTypeDTO.sum && isMonotonic === false;
|
||||
|
||||
switch (metricType) {
|
||||
case MetrictypesTypeDTO.sum:
|
||||
@@ -135,7 +131,7 @@ export function getMetricDetailsQuery(
|
||||
break;
|
||||
}
|
||||
|
||||
const attributeType = toAttributeType(metricType, isMonotonic, temporality);
|
||||
const attributeType = toAttributeType(metricType, isMonotonic);
|
||||
|
||||
return {
|
||||
...initialQueriesMap[DataSource.METRICS],
|
||||
|
||||
@@ -393,13 +393,12 @@ describe('selecting a metric type updates the aggregation options', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('cumulative non-monotonic Sum metric is treated as Gauge', () => {
|
||||
it('non-monotonic Sum metric is treated as Gauge', () => {
|
||||
returnMetrics([
|
||||
makeMetric({
|
||||
metricName: 'active_connections',
|
||||
type: MetrictypesTypeDTO.sum,
|
||||
isMonotonic: false,
|
||||
temporality: 'cumulative' as never,
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -428,36 +427,6 @@ describe('selecting a metric type updates the aggregation options', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('delta non-monotonic Sum metric is treated as Sum', () => {
|
||||
returnMetrics([
|
||||
makeMetric({
|
||||
metricName: 'queue_depth_delta',
|
||||
type: MetrictypesTypeDTO.sum,
|
||||
isMonotonic: false,
|
||||
temporality: 'delta' as never,
|
||||
}),
|
||||
]);
|
||||
|
||||
render(<MetricQueryHarness query={makeQuery()} />);
|
||||
|
||||
const input = screen.getByRole('combobox');
|
||||
fireEvent.change(input, {
|
||||
target: { value: 'queue_depth_delta' },
|
||||
});
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(getOptionLabels('time-agg-options')).toStrictEqual([
|
||||
'Rate',
|
||||
'Increase',
|
||||
]);
|
||||
expect(getOptionLabels('space-agg-options')).toStrictEqual([
|
||||
'Sum',
|
||||
'Avg',
|
||||
'Min',
|
||||
'Max',
|
||||
]);
|
||||
});
|
||||
|
||||
it('Histogram metric shows no time options and P50–P99 space options', () => {
|
||||
returnMetrics([
|
||||
makeMetric({
|
||||
|
||||
@@ -34,7 +34,7 @@ export type MetricNameSelectorProps = {
|
||||
function getAttributeType(
|
||||
metric: MetricsexplorertypesListMetricDTO,
|
||||
): ATTRIBUTE_TYPES | '' {
|
||||
return toAttributeType(metric.type, metric.isMonotonic, metric.temporality);
|
||||
return toAttributeType(metric.type, metric.isMonotonic);
|
||||
}
|
||||
|
||||
function createAutocompleteData(
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import { defaultFeatureFlags, render, screen } from 'tests/test-utils';
|
||||
import {
|
||||
invalidLicense,
|
||||
setupAuthzAdmin,
|
||||
@@ -58,6 +59,23 @@ function renderEditPage(
|
||||
|
||||
describe('CreateEditRolePage - Feature Gate', () => {
|
||||
describe('create mode - feature disabled', () => {
|
||||
it('shows error when fine-grained authz flag is inactive', async () => {
|
||||
renderCreatePage({
|
||||
featureFlags: defaultFeatureFlags.map((f) =>
|
||||
f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ
|
||||
? { ...f, active: false }
|
||||
: f,
|
||||
),
|
||||
});
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('feature-gate-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(/Custom roles feature is not available/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error when license is invalid', async () => {
|
||||
renderCreatePage({ activeLicense: invalidLicense });
|
||||
|
||||
@@ -95,6 +113,23 @@ describe('CreateEditRolePage - Feature Gate', () => {
|
||||
const ROLE_ID = '019c24aa-3333-0001-aaaa-111111111111';
|
||||
const ROLE_NAME = 'test-role';
|
||||
|
||||
it('shows error when fine-grained authz flag is inactive', async () => {
|
||||
renderEditPage(ROLE_ID, ROLE_NAME, {
|
||||
featureFlags: defaultFeatureFlags.map((f) =>
|
||||
f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ
|
||||
? { ...f, active: false }
|
||||
: f,
|
||||
),
|
||||
});
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('feature-gate-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(/Custom roles feature is not available/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error when license is invalid', async () => {
|
||||
renderEditPage(ROLE_ID, ROLE_NAME, { activeLicense: invalidLicense });
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as roleApi from 'api/generated/services/role';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
import { defaultFeatureFlags, render, screen, waitFor } from 'tests/test-utils';
|
||||
import {
|
||||
invalidLicense,
|
||||
setupAuthzAdmin,
|
||||
@@ -32,6 +33,26 @@ describe('ViewRolePage - Feature Gate', () => {
|
||||
});
|
||||
|
||||
describe('feature disabled', () => {
|
||||
it('shows error when fine-grained authz flag is inactive', async () => {
|
||||
render(<ViewRolePage />, undefined, {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
appContextOverrides: {
|
||||
featureFlags: defaultFeatureFlags.map((f) =>
|
||||
f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ
|
||||
? { ...f, active: false }
|
||||
: f,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('feature-gate-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(/Custom roles feature is not available/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error when license is invalid', async () => {
|
||||
render(<ViewRolePage />, undefined, {
|
||||
initialRoute: buildViewRoleRoute(CUSTOM_ROLE_ID, CUSTOM_ROLE_NAME),
|
||||
|
||||
@@ -4,7 +4,13 @@ import {
|
||||
} from 'mocks-server/__mockdata__/roles';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import {
|
||||
defaultFeatureFlags,
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
} from 'tests/test-utils';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import {
|
||||
invalidLicense,
|
||||
setupAuthzAdmin,
|
||||
@@ -185,6 +191,30 @@ describe('RolesSettings', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('hides the create button and disables row clicks when fine-grained authz flag is inactive', async () => {
|
||||
render(<RolesSettings />, undefined, {
|
||||
appContextOverrides: {
|
||||
featureFlags: defaultFeatureFlags.map((f) =>
|
||||
f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ
|
||||
? { ...f, active: false }
|
||||
: f,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /custom role/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const rows = document.querySelectorAll('.roles-table-row');
|
||||
rows.forEach((row) => {
|
||||
expect(row).not.toHaveClass('roles-table-row--clickable');
|
||||
expect(row.getAttribute('role')).not.toBe('button');
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the create button and disables row clicks when license is not valid', async () => {
|
||||
render(<RolesSettings />, undefined, {
|
||||
appContextOverrides: { activeLicense: invalidLicense },
|
||||
|
||||
@@ -2,15 +2,10 @@
|
||||
* This was introduced to fix a sync bug between Nuqs and react-router-dom
|
||||
*
|
||||
* We are using the wrong adapter for nuqs because the correct one only supports v6/v7,
|
||||
* and we are at version v5. Nuqs writes params straight to the History API, which
|
||||
* react-router v5 never observes, so `useLocation().search` (and `useUrlQuery()`) can
|
||||
* be several nuqs updates behind the real URL.
|
||||
* and we are at version v5. This causes the nuqs/react-router-dom to be out of sync.
|
||||
*
|
||||
* Use this whenever you need to build a navigation target on top of the current
|
||||
* params, otherwise stale values get republished and nuqs adopts them back on its
|
||||
* next flush (it snapshots `window.location.search`).
|
||||
*
|
||||
* We can revert this once we migrate react-router-dom to v6.
|
||||
* We can revert this commit once we migrate react-router-dom to v6, or once we migrate
|
||||
* to DateTimeSelectionV3
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
11
frontend/src/hooks/useIsInfraMonitoringV2.ts
Normal file
11
frontend/src/hooks/useIsInfraMonitoringV2.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
export function useIsInfraMonitoringV2(): boolean {
|
||||
const { featureFlags } = useAppContext();
|
||||
return Boolean(
|
||||
featureFlags?.find(
|
||||
(flag) => flag.name === FeatureKeys.USE_INFRA_MONITORING_V2,
|
||||
)?.active,
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
|
||||
@@ -5,12 +6,22 @@ export const useRolesFeatureGate = (): {
|
||||
isRolesEnabled: boolean;
|
||||
isLoading: boolean;
|
||||
} => {
|
||||
const { activeLicense, isFetchingActiveLicense } = useAppContext();
|
||||
const {
|
||||
activeLicense,
|
||||
featureFlags,
|
||||
isFetchingActiveLicense,
|
||||
isFetchingFeatureFlags,
|
||||
} = useAppContext();
|
||||
|
||||
const isValidLicense = activeLicense?.status === LicenseStatus.VALID;
|
||||
const isFineGrainedAuthzEnabled =
|
||||
featureFlags?.find((f) => f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ)
|
||||
?.active ?? false;
|
||||
|
||||
return {
|
||||
isRolesEnabled: isValidLicense,
|
||||
isLoading: isFetchingActiveLicense && !activeLicense,
|
||||
isRolesEnabled: isValidLicense && isFineGrainedAuthzEnabled,
|
||||
isLoading:
|
||||
(isFetchingActiveLicense && !activeLicense) ||
|
||||
(isFetchingFeatureFlags && !featureFlags),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import getMinAgo from './getStartAndEndTime/getMinAgo';
|
||||
|
||||
const validCustomTimeRegex = /^(\d+)(months?|[mhdw])$/;
|
||||
const validCustomTimeRegex = /^(\d+)([mhdw])$/;
|
||||
|
||||
export const isValidShortHandDateTimeFormat = (time: string): boolean =>
|
||||
validCustomTimeRegex.test(time);
|
||||
|
||||
@@ -52,12 +52,12 @@ const makeDashboard = (
|
||||
...overrides,
|
||||
}) as unknown as DashboardListItem;
|
||||
|
||||
const renderRow = (dashboard: DashboardListItem, canEdit = true): void => {
|
||||
const renderRow = (dashboard: DashboardListItem): void => {
|
||||
render(
|
||||
<DashboardRow
|
||||
dashboard={dashboard}
|
||||
index={0}
|
||||
canEdit={canEdit}
|
||||
canEdit
|
||||
showUpdatedAt={false}
|
||||
showUpdatedBy={false}
|
||||
/>,
|
||||
@@ -105,19 +105,6 @@ describe('DashboardRow', () => {
|
||||
|
||||
expect(mockSafeNavigate).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('legacy-dashboard-id')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('legacy-dashboard-retry-migration'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('withholds the retry action from a row the user cannot edit', async () => {
|
||||
renderRow(makeDashboard({ legacy: true }), false);
|
||||
|
||||
await userEvent.click(screen.getByTestId('dashboard-title-0'));
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('legacy-dashboard-retry-migration'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -252,7 +252,6 @@ function DashboardRow({
|
||||
open={isLegacyDialogOpen}
|
||||
dashboardId={id}
|
||||
dashboardName={name}
|
||||
canEdit={canEdit}
|
||||
onClose={(): void => setIsLegacyDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -18,30 +18,19 @@ jest.mock('container/Integrations/utils', () => ({
|
||||
handleContactSupport: (isCloud: boolean): void => mockContactSupport(isCloud),
|
||||
}));
|
||||
|
||||
const mockRetryMigration = jest.fn();
|
||||
let isMigrating = false;
|
||||
jest.mock('../../hooks/useRetryMigration', () => ({
|
||||
useRetryMigration: (): {
|
||||
retryMigration: jest.Mock;
|
||||
isMigrating: boolean;
|
||||
} => ({ retryMigration: mockRetryMigration, isMigrating }),
|
||||
}));
|
||||
|
||||
const DASHBOARD_ID = '0f9a1b2c-3d4e-5f6a-7b8c-9d0e1f2a3b4c';
|
||||
|
||||
describe('LegacyDashboardDialog', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
isMigrating = false;
|
||||
});
|
||||
|
||||
const setup = ({ open = true, canEdit = true } = {}): void => {
|
||||
const setup = (open = true): void => {
|
||||
render(
|
||||
<LegacyDashboardDialog
|
||||
open={open}
|
||||
dashboardId={DASHBOARD_ID}
|
||||
dashboardName="My Legacy Dashboard"
|
||||
canEdit={canEdit}
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
@@ -68,31 +57,8 @@ describe('LegacyDashboardDialog', () => {
|
||||
expect(mockContactSupport).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('retries the migration for the dashboard', async () => {
|
||||
setup();
|
||||
await userEvent.click(screen.getByTestId('legacy-dashboard-retry-migration'));
|
||||
expect(mockRetryMigration).toHaveBeenCalledWith(DASHBOARD_ID);
|
||||
});
|
||||
|
||||
it('blocks retry and close while the migration is in flight', () => {
|
||||
isMigrating = true;
|
||||
setup();
|
||||
expect(screen.getByTestId('legacy-dashboard-retry-migration')).toBeDisabled();
|
||||
expect(screen.getByTestId('legacy-dashboard-close')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('offers only the support path without edit access', () => {
|
||||
setup({ canEdit: false });
|
||||
expect(
|
||||
screen.queryByTestId('legacy-dashboard-retry-migration'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('legacy-dashboard-contact-support'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders nothing when closed', () => {
|
||||
setup({ open: false });
|
||||
setup(false);
|
||||
expect(screen.queryByTestId('legacy-dashboard-id')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ArrowUpRight, Copy, RotateCw } from '@signozhq/icons';
|
||||
import { ArrowUpRight, Copy } from '@signozhq/icons';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -9,34 +9,28 @@ import { handleContactSupport } from 'container/Integrations/utils';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { DashboardListEvents } from 'pages/DashboardsListPageV2/constants/events';
|
||||
|
||||
import { useRetryMigration } from '../../hooks/useRetryMigration';
|
||||
|
||||
import styles from './LegacyDashboardDialog.module.scss';
|
||||
|
||||
interface LegacyDashboardDialogProps {
|
||||
open: boolean;
|
||||
dashboardId: string;
|
||||
dashboardName: string;
|
||||
canEdit: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explains why a legacy (pre-v2) dashboard can't be opened in the new experience
|
||||
* and offers to re-run the migration. Legacy rows are surfaced by the list API
|
||||
* with `legacy: true` but have no v2 spec to render. Retrying needs edit access,
|
||||
* so viewers only get the dashboard ID to share with support.
|
||||
* and hands the user the dashboard ID to share with support. Legacy rows are
|
||||
* surfaced by the list API with `legacy: true` but have no v2 spec to render.
|
||||
*/
|
||||
function LegacyDashboardDialog({
|
||||
open,
|
||||
dashboardId,
|
||||
dashboardName,
|
||||
canEdit,
|
||||
onClose,
|
||||
}: LegacyDashboardDialogProps): JSX.Element {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const { isCloudUser } = useGetTenantLicense();
|
||||
const { retryMigration, isMigrating } = useRetryMigration(onClose);
|
||||
|
||||
const onCopyId = (): void => {
|
||||
copyToClipboard(dashboardId);
|
||||
@@ -55,14 +49,6 @@ function LegacyDashboardDialog({
|
||||
});
|
||||
};
|
||||
|
||||
const onRetryMigration = (): void => {
|
||||
retryMigration(dashboardId);
|
||||
void logEvent(DashboardListEvents.LegacyDialogAction, {
|
||||
action: 'retryMigration',
|
||||
dashboardId,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogWrapper
|
||||
title="This dashboard isn't available in the new experience"
|
||||
@@ -79,15 +65,14 @@ function LegacyDashboardDialog({
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="md"
|
||||
disabled={isMigrating}
|
||||
onClick={onClose}
|
||||
testId="legacy-dashboard-close"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant={canEdit ? 'outlined' : 'solid'}
|
||||
color={canEdit ? 'secondary' : 'primary'}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="md"
|
||||
suffix={<ArrowUpRight size={14} />}
|
||||
onClick={onContactSupport}
|
||||
@@ -95,20 +80,6 @@ function LegacyDashboardDialog({
|
||||
>
|
||||
Contact Support
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="md"
|
||||
prefix={<RotateCw size={14} />}
|
||||
disabled={isMigrating}
|
||||
loading={isMigrating}
|
||||
onClick={onRetryMigration}
|
||||
testId="legacy-dashboard-retry-migration"
|
||||
>
|
||||
Retry migration
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -116,10 +87,8 @@ function LegacyDashboardDialog({
|
||||
<Typography.Text className={styles.description}>
|
||||
<strong>{dashboardName || 'This dashboard'}</strong> hasn't been
|
||||
migrated to the new dashboard experience yet, so it can't be opened
|
||||
here.{' '}
|
||||
{canEdit
|
||||
? "Retrying the migration often works once we've handled the case that blocked it. If it still fails, share the dashboard ID below with support."
|
||||
: "Share the dashboard ID below with support and we'll help you move it over."}
|
||||
here. Share the dashboard ID below with support and we'll help you
|
||||
move it over.
|
||||
</Typography.Text>
|
||||
|
||||
<div className={styles.idField}>
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import {
|
||||
invalidateListDashboardsForUserV2,
|
||||
useMigrateDashboardV2,
|
||||
} from 'api/generated/services/dashboard';
|
||||
|
||||
import { useRetryMigration } from '../useRetryMigration';
|
||||
|
||||
jest.mock('react-query', () => ({
|
||||
useQueryClient: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('api/generated/services/dashboard', () => ({
|
||||
useMigrateDashboardV2: jest.fn(),
|
||||
invalidateListDashboardsForUserV2: jest.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
const queryClient = { invalidateQueries: jest.fn() };
|
||||
const mockMutate = jest.fn();
|
||||
const onMigrated = jest.fn();
|
||||
|
||||
type MutationHandlers = {
|
||||
onSuccess: () => Promise<void>;
|
||||
onError: (error: unknown) => void;
|
||||
};
|
||||
|
||||
let captured: MutationHandlers;
|
||||
|
||||
// Stands in for the generated mutation hook: records the handlers the hook wires
|
||||
// up so each one can be driven directly, and reports the requested in-flight state.
|
||||
function setup(isLoading = false): {
|
||||
retryMigration: (id: string) => void;
|
||||
isMigrating: boolean;
|
||||
} {
|
||||
(useMigrateDashboardV2 as jest.Mock).mockImplementation(
|
||||
(options: { mutation: MutationHandlers }) => {
|
||||
captured = options.mutation;
|
||||
return { mutate: mockMutate, isLoading };
|
||||
},
|
||||
);
|
||||
return renderHook(() => useRetryMigration(onMigrated)).result.current;
|
||||
}
|
||||
|
||||
// A 501 from GET/POST on an un-migrated dashboard carries the render error envelope.
|
||||
const envelopeError = {
|
||||
response: {
|
||||
status: 501,
|
||||
data: {
|
||||
error: { code: 'dashboard_invalid_data', message: 'not in v6 schema' },
|
||||
},
|
||||
},
|
||||
message: 'Request failed with status code 501',
|
||||
};
|
||||
|
||||
// A gateway failure responds without an envelope, so there is no backend reason to show.
|
||||
const bodylessError = {
|
||||
response: { status: 502, data: '<html>bad gateway</html>' },
|
||||
message: 'Request failed with status code 502',
|
||||
};
|
||||
|
||||
describe('useRetryMigration', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useQueryClient as jest.Mock).mockReturnValue(queryClient);
|
||||
});
|
||||
|
||||
it('sends the dashboard id as a path parameter', () => {
|
||||
setup().retryMigration('dash-1');
|
||||
expect(mockMutate).toHaveBeenCalledWith({ pathParams: { id: 'dash-1' } });
|
||||
});
|
||||
|
||||
it('refreshes the list, confirms with a toast and reports success', async () => {
|
||||
setup();
|
||||
await captured.onSuccess();
|
||||
|
||||
expect(invalidateListDashboardsForUserV2).toHaveBeenCalledWith(queryClient);
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
'Dashboard migrated to the new experience',
|
||||
);
|
||||
expect(onMigrated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('surfaces the backend reason and does not report success on failure', () => {
|
||||
setup();
|
||||
captured.onError(envelopeError);
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('not in v6 schema');
|
||||
expect(onMigrated).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('points the user at support when the failure carries no reason', () => {
|
||||
setup();
|
||||
captured.onError(bodylessError);
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
'Could not migrate this dashboard. Please contact support.',
|
||||
);
|
||||
});
|
||||
|
||||
it('reports the in-flight state from the mutation', () => {
|
||||
expect(setup(true).isMigrating).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import {
|
||||
invalidateListDashboardsForUserV2,
|
||||
useMigrateDashboardV2,
|
||||
} from 'api/generated/services/dashboard';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
const FAILURE_MESSAGE =
|
||||
'Could not migrate this dashboard. Please contact support.';
|
||||
|
||||
export interface UseRetryMigrationResult {
|
||||
// Re-run the v1 to v2 migration for a dashboard.
|
||||
retryMigration: (id: string) => void;
|
||||
isMigrating: boolean;
|
||||
}
|
||||
|
||||
// Wraps the retry-migration mutation for a legacy (pre-v2) dashboard: refreshes
|
||||
// the personalized list so the row loses its legacy flag, and reports the
|
||||
// backend's reason as a toast when the dashboard still can't be converted.
|
||||
export function useRetryMigration(
|
||||
onMigrated?: () => void,
|
||||
): UseRetryMigrationResult {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const migrate = useMigrateDashboardV2({
|
||||
mutation: {
|
||||
onSuccess: async (): Promise<void> => {
|
||||
await invalidateListDashboardsForUserV2(queryClient);
|
||||
toast.success('Dashboard migrated to the new experience');
|
||||
onMigrated?.();
|
||||
},
|
||||
onError: (error): void => {
|
||||
toast.error(toAPIError(error, FAILURE_MESSAGE).getErrorMessage());
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const retryMigration = useCallback(
|
||||
(id: string): void => {
|
||||
migrate.mutate({ pathParams: { id } });
|
||||
},
|
||||
[migrate],
|
||||
);
|
||||
|
||||
return { retryMigration, isMigrating: migrate.isLoading };
|
||||
}
|
||||
@@ -114,23 +114,6 @@ describe('getCaretContext — stage detection', () => {
|
||||
expect(ctx.partial).toBe('');
|
||||
});
|
||||
|
||||
it('never replaces past the caret when it sits before the operator', () => {
|
||||
const ctx = getCaretContext("env = 'prod'", 4);
|
||||
expect(ctx.stage).toBe('operator');
|
||||
expect(ctx.partial).toBe('');
|
||||
expect(ctx.replaceStart).toBe(4);
|
||||
expect(ctx.replaceEnd).toBe(4);
|
||||
});
|
||||
|
||||
it('never replaces past the caret when it sits before the value', () => {
|
||||
const ctx = getCaretContext("env = 'prod'", 6);
|
||||
expect(ctx.stage).toBe('value');
|
||||
expect(ctx.operator).toBe('=');
|
||||
expect(ctx.partial).toBe('');
|
||||
expect(ctx.replaceStart).toBe(6);
|
||||
expect(ctx.replaceEnd).toBe(6);
|
||||
});
|
||||
|
||||
it('detects the stage of the term under a mid-string caret', () => {
|
||||
const q = "env = AND team = 'core'";
|
||||
// caret right after the first `env ` (index 4) is the operator stage
|
||||
@@ -159,13 +142,6 @@ describe('spliceAtCaret', () => {
|
||||
expect(next).toBe("env = 'prod'");
|
||||
});
|
||||
|
||||
it('inserts (without duplicating text) at a caret parked before a token', () => {
|
||||
const q = "env = 'prod'";
|
||||
const ctx = getCaretContext(q, 4);
|
||||
const { next } = spliceAtCaret(q, ctx, '!= ');
|
||||
expect(next).toBe("env != = 'prod'");
|
||||
});
|
||||
|
||||
it('preserves text after the caret', () => {
|
||||
const q = "env AND team = 'core'";
|
||||
const ctx = getCaretContext(q, 4); // operator gap after `env`
|
||||
|
||||
@@ -328,7 +328,7 @@ export const getCaretContext = (query: string, caret: number): CaretContext => {
|
||||
fieldKey: scan.key ? scan.key.text : '',
|
||||
operator: slot.operator,
|
||||
partial: slot.partial,
|
||||
replaceStart: Math.min(term.start + slot.replaceStartRel, pos),
|
||||
replaceStart: term.start + slot.replaceStartRel,
|
||||
replaceEnd: pos,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,15 +1,47 @@
|
||||
import { Suspense } from 'react';
|
||||
import Loadable from 'components/Loadable';
|
||||
import { TabRoutes } from 'components/RouteTab/types';
|
||||
import Spinner from 'components/Spinner';
|
||||
import ROUTES from 'constants/routes';
|
||||
import InfraMonitoringHostsV2 from 'container/InfraMonitoringHostsV2';
|
||||
import InfraMonitoringK8sV2 from 'container/InfraMonitoringK8sV2';
|
||||
import InfraMonitoringHosts from 'container/InfraMonitoringHosts';
|
||||
import InfraMonitoringK8s from 'container/InfraMonitoringK8s';
|
||||
import { useIsInfraMonitoringV2 } from 'hooks/useIsInfraMonitoringV2';
|
||||
import { Inbox } from '@signozhq/icons';
|
||||
|
||||
const InfraMonitoringHostsV2 = Loadable(
|
||||
() => import('container/InfraMonitoringHostsV2'),
|
||||
);
|
||||
|
||||
const InfraMonitoringK8sV2 = Loadable(
|
||||
() => import('container/InfraMonitoringK8sV2'),
|
||||
);
|
||||
|
||||
function HostsContainer(): JSX.Element {
|
||||
return <InfraMonitoringHostsV2 />;
|
||||
const isInfraMonitoringV2 = useIsInfraMonitoringV2();
|
||||
|
||||
if (isInfraMonitoringV2) {
|
||||
return (
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<InfraMonitoringHostsV2 />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <InfraMonitoringHosts />;
|
||||
}
|
||||
|
||||
function KubernetesContainer(): JSX.Element {
|
||||
return <InfraMonitoringK8sV2 />;
|
||||
const isInfraMonitoringV2 = useIsInfraMonitoringV2();
|
||||
|
||||
if (isInfraMonitoringV2) {
|
||||
return (
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<InfraMonitoringK8sV2 />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <InfraMonitoringK8s />;
|
||||
}
|
||||
|
||||
export const Hosts: TabRoutes = {
|
||||
|
||||
@@ -150,6 +150,13 @@ export const defaultFeatureFlags = [
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
name: FeatureKeys.USE_FINE_GRAINED_AUTHZ,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
];
|
||||
|
||||
export function getAppContextMock(
|
||||
|
||||
@@ -69,12 +69,7 @@ export const getMetricsExplorerUrl = ({
|
||||
const params = new URLSearchParams();
|
||||
params.set(
|
||||
QueryParams.compositeQuery,
|
||||
// `unit` must always be present: the query builder provider rewrites (and
|
||||
// pushes a new history entry for) any compositeQuery missing a key of
|
||||
// `initialQueriesMap`, which traps the browser back button.
|
||||
// Since this is only being used by infra-monitoring, I will keep this fix one line
|
||||
// instead of going and update each chart configuration.
|
||||
encodeURIComponent(JSON.stringify({ unit: '', ...query })),
|
||||
encodeURIComponent(JSON.stringify(query)),
|
||||
);
|
||||
|
||||
if (relativeTime) {
|
||||
|
||||
@@ -112,12 +112,15 @@ functionCall
|
||||
;
|
||||
|
||||
/*
|
||||
* Full-text search: search('term') or scoped search('term', body, ...).
|
||||
* First param is the search term; the rest are field-context scopes (body/attribute/
|
||||
* resource/log), quoted or bare. Handled in the visitor — no grammar change.
|
||||
* Full-text search call: search('needle')
|
||||
*
|
||||
* Uses the shared functionParamList so future scoped forms like
|
||||
* search(body, 'abc') / search(attribute, 'abc') need no grammar change. Today
|
||||
* only a single needle is supported. Unlike bare/quoted free text (`fullText`),
|
||||
* which only targets the body column, search() fans out across every field.
|
||||
*/
|
||||
searchCall
|
||||
: SEARCH LPAREN valueList RPAREN
|
||||
: SEARCH LPAREN functionParamList RPAREN
|
||||
;
|
||||
|
||||
// Function parameters can be keys, single scalar values, or arrays
|
||||
|
||||
@@ -241,12 +241,9 @@ func (server *Server) PutAlerts(ctx context.Context, postableAlerts alertmanager
|
||||
}
|
||||
|
||||
func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertmanagertypes.Config) error {
|
||||
resolved, err := alertmanagerConfig.Resolved()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := resolved.AlertmanagerConfig()
|
||||
config := alertmanagerConfig.AlertmanagerConfig()
|
||||
|
||||
var err error
|
||||
// Load SigNoz's alertmanager notification templates from the configured
|
||||
// globs. The upstream default templates (default.tmpl, email.tmpl) are
|
||||
// always loaded from the embedded alertmanager assets inside FromGlobs, so
|
||||
@@ -278,7 +275,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
|
||||
server.logger.InfoContext(ctx, "skipping creation of receiver not referenced by any route", slog.String("receiver", rcv.Name))
|
||||
continue
|
||||
}
|
||||
extendedRcv, err := resolved.GetReceiver(rcv.Name)
|
||||
extendedRcv, err := alertmanagerConfig.GetReceiver(rcv.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -353,7 +350,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
|
||||
go server.dispatcher.Run()
|
||||
go server.inhibitor.Run()
|
||||
|
||||
server.alertmanagerConfig = resolved
|
||||
server.alertmanagerConfig = alertmanagerConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package alertmanager
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -25,6 +26,11 @@ type Signoz struct {
|
||||
alertmanagerserver.Config `mapstructure:",squash" yaml:",squash"`
|
||||
}
|
||||
|
||||
type Legacy struct {
|
||||
// ApiURL is the URL of the legacy signoz alertmanager.
|
||||
ApiURL *url.URL `mapstructure:"api_url"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
return factory.NewConfigFactory(factory.MustNewName("alertmanager"), newConfig)
|
||||
}
|
||||
|
||||
@@ -167,10 +167,6 @@ func (provider *provider) UpdateChannelByReceiverAndID(ctx context.Context, orgI
|
||||
return err
|
||||
}
|
||||
|
||||
if err := config.SetGlobalConfig(provider.config.Signoz.Global); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := config.UpdateReceiver(receiver); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -221,10 +217,6 @@ func (provider *provider) CreateChannel(ctx context.Context, orgID string, recei
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := config.SetGlobalConfig(provider.config.Signoz.Global); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := config.CreateReceiver(receiver); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -85,23 +85,6 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}/migrate", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.MigrateV2), handler.OpenAPIDef{
|
||||
ID: "MigrateDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Migrate dashboard to v2",
|
||||
Description: "This endpoint retries the v1→v2 (Perses) migration on a dashboard still stored in the v1 schema and returns the v2-shape result. It is idempotent: a dashboard already in the v2 schema is returned unchanged.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(dashboardtypes.GettableDashboardV2),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.dashboardHandler.GetV2), handler.OpenAPIDef{
|
||||
ID: "GetDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
|
||||
@@ -29,18 +29,13 @@ func New(t *testing.T) flagger.Flagger {
|
||||
|
||||
// WithUseJSONBody returns a Flagger with use_json_body set to the given value.
|
||||
func WithUseJSONBody(t *testing.T, enabled bool) flagger.Flagger {
|
||||
return WithBooleanFlags(t, map[string]bool{
|
||||
flagger.FeatureUseJSONBody.String(): enabled,
|
||||
})
|
||||
}
|
||||
|
||||
// WithBooleanFlags returns a Flagger with the given boolean flags, keyed by feature name.
|
||||
func WithBooleanFlags(t *testing.T, flags map[string]bool) flagger.Flagger {
|
||||
t.Helper()
|
||||
registry := flagger.MustNewRegistry()
|
||||
cfg := flagger.Config{}
|
||||
if len(flags) > 0 {
|
||||
cfg.Config.Boolean = flags
|
||||
if enabled {
|
||||
cfg.Config.Boolean = map[string]bool{
|
||||
flagger.FeatureUseJSONBody.String(): true,
|
||||
}
|
||||
}
|
||||
fl, err := flagger.New(
|
||||
context.Background(),
|
||||
|
||||
@@ -10,8 +10,11 @@ var (
|
||||
FeaturePutMetersInZeus = featuretypes.MustNewName("put_meters_in_zeus")
|
||||
FeatureUseMeterReporter = featuretypes.MustNewName("use_meter_reporter")
|
||||
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
|
||||
FeatureUseFineGrainedAuthz = featuretypes.MustNewName("use_fine_grained_authz")
|
||||
FeatureEnableAIObservability = featuretypes.MustNewName("enable_ai_observability")
|
||||
FeatureEnableMetricsReduction = featuretypes.MustNewName("enable_metrics_reduction")
|
||||
FeatureUseInfraMonitoringV2 = featuretypes.MustNewName("use_infra_monitoring_v2")
|
||||
|
||||
FeatureUsePrometheusClickhouseV2 = featuretypes.MustNewName("use_prometheus_clickhouse_v2")
|
||||
)
|
||||
|
||||
@@ -73,6 +76,14 @@ func MustNewRegistry() featuretypes.Registry {
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureUseFineGrainedAuthz,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
Stage: featuretypes.StageExperimental,
|
||||
Description: "Controls whether fine-grained authorization is enabled",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureEnableAIObservability,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
@@ -89,6 +100,14 @@ func MustNewRegistry() featuretypes.Registry {
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureUseInfraMonitoringV2,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
Stage: featuretypes.StageExperimental,
|
||||
Description: "Controls whether infra monitoring v2 is enabled",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureUsePrometheusClickhouseV2,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package implcloudintegration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
citypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServiceDefinitionsAreValid(t *testing.T) {
|
||||
store := NewServiceDefinitionStore()
|
||||
|
||||
for _, provider := range []citypes.CloudProviderType{
|
||||
citypes.CloudProviderTypeAWS,
|
||||
citypes.CloudProviderTypeAzure,
|
||||
citypes.CloudProviderTypeGCP,
|
||||
} {
|
||||
t.Run(provider.StringValue(), func(t *testing.T) {
|
||||
defs, err := store.List(context.Background(), provider)
|
||||
require.NoError(t, err, "all embedded definitions must load and validate")
|
||||
require.NotEmpty(t, defs, "provider should ship at least one service definition")
|
||||
|
||||
for _, def := range defs {
|
||||
assert.NotEmpty(t, def.ID, "service definition must have an id")
|
||||
assert.NotEmpty(t, def.Title, "service %q must have a title", def.ID)
|
||||
|
||||
// Get() must agree with List() for every service it advertises.
|
||||
serviceID, err := citypes.NewServiceID(provider, def.ID)
|
||||
if !assert.NoError(t, err, "service id %q must be registered in serviceid.go", def.ID) {
|
||||
continue
|
||||
}
|
||||
got, err := store.Get(context.Background(), provider, serviceID)
|
||||
require.NoError(t, err, "service %q listed but not gettable", def.ID)
|
||||
assert.Equal(t, def.ID, got.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -621,7 +621,7 @@
|
||||
{
|
||||
"metricName": "cloudsql.googleapis.com/database/postgresql/deadlock_count",
|
||||
"temporality": "",
|
||||
"timeAggregation": "rate",
|
||||
"timeAggregation": "max",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
@@ -882,7 +882,7 @@
|
||||
{
|
||||
"metricName": "cloudsql.googleapis.com/database/postgresql/transaction_count",
|
||||
"temporality": "",
|
||||
"timeAggregation": "rate",
|
||||
"timeAggregation": "max",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
|
||||
@@ -54,13 +54,13 @@
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/postgresql/transaction_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/postgresql/deadlock_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
|
||||
@@ -925,7 +925,7 @@
|
||||
"metricName": "compute.googleapis.com/instance/disk/average_io_latency",
|
||||
"temporality": "",
|
||||
"timeAggregation": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1207,8 +1207,8 @@
|
||||
{
|
||||
"metricName": "kubernetes.io/container/restart_count",
|
||||
"temporality": "",
|
||||
"timeAggregation": "increase",
|
||||
"spaceAggregation": "sum",
|
||||
"timeAggregation": "max",
|
||||
"spaceAggregation": "max",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -601,8 +601,8 @@
|
||||
{
|
||||
"metricName": "redis.googleapis.com/stats/cache_hit_ratio",
|
||||
"temporality": "",
|
||||
"timeAggregation": "min",
|
||||
"spaceAggregation": "min",
|
||||
"timeAggregation": "max",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
],
|
||||
@@ -788,7 +788,7 @@
|
||||
"metricName": "redis.googleapis.com/commands/usec_per_call",
|
||||
"temporality": "",
|
||||
"timeAggregation": "max",
|
||||
"spaceAggregation": "max",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
],
|
||||
@@ -945,7 +945,7 @@
|
||||
{
|
||||
"metricName": "redis.googleapis.com/commands/calls",
|
||||
"temporality": "",
|
||||
"timeAggregation": "rate",
|
||||
"timeAggregation": "max",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
@@ -1044,8 +1044,8 @@
|
||||
{
|
||||
"metricName": "redis.googleapis.com/stats/reject_connections_count",
|
||||
"temporality": "",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum",
|
||||
"timeAggregation": "avg",
|
||||
"spaceAggregation": "avg",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
],
|
||||
@@ -1213,4 +1213,4 @@
|
||||
"refreshInterval": "",
|
||||
"links": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,9 +63,6 @@ type Module interface {
|
||||
|
||||
GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
// MigrateV2 retries the v1→v2 migration on a dashboard still stored in the v1 schema.
|
||||
MigrateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
ListV2(ctx context.Context, orgID valuer.UUID, params *dashboardtypes.ListDashboardsV2Params) (*dashboardtypes.ListableDashboardV2, error)
|
||||
|
||||
ListForUserV2(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, params *dashboardtypes.ListDashboardsV2Params) (*dashboardtypes.ListableDashboardForUserV2, error)
|
||||
@@ -135,8 +132,6 @@ type Handler interface {
|
||||
|
||||
GetV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
MigrateV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
ListV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
ListForUserV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
@@ -207,38 +207,6 @@ func (handler *handler) GetV2(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Success(rw, http.StatusOK, dashboard.ToGettableDashboardV2())
|
||||
}
|
||||
|
||||
func (handler *handler) MigrateV2(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.MigrateV2(ctx, orgID, dashboardID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboard.ToGettableDashboardV2())
|
||||
}
|
||||
|
||||
func (handler *handler) LockV2(rw http.ResponseWriter, r *http.Request) {
|
||||
handler.lockUnlockV2(rw, r, true)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/transition"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
@@ -122,51 +121,6 @@ func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UU
|
||||
return storable.ToDashboardV2(tags)
|
||||
}
|
||||
|
||||
// MigrateV2 retries the v1→v2 migration on a dashboard still stored as v1 (one the
|
||||
// bulk 103 migration skipped or failed). Idempotent: an already-v2 one is unchanged.
|
||||
func (module *module) MigrateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
|
||||
storable, err := module.store.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Already migrated: return as-is.
|
||||
if storable.IsV2() {
|
||||
tags, err := module.tagModule.ListForResource(ctx, orgID, coretypes.KindDashboard, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return storable.ToDashboardV2(tags)
|
||||
}
|
||||
|
||||
// v1→v2 needs v5-shaped queries; run v4→v5 in place first.
|
||||
transition.NewDashboardMigrateV5(module.settings.Logger(), nil, nil).Migrate(ctx, storable.Data)
|
||||
|
||||
v2, err := storable.ConvertV1ToV2()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
resolvedTags, err := module.tagModule.SyncTags(ctx, orgID, coretypes.KindDashboard, v2.ID, tagtypes.NewPostableTagsFromTags(v2.Tags))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v2.Tags = resolvedTags
|
||||
|
||||
storableV2, err := v2.ToStorableDashboard()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return module.store.Update(ctx, orgID, storableV2)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v2, nil
|
||||
}
|
||||
|
||||
func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatable dashboardtypes.UpdatableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
if err := updatable.Validate(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -213,18 +213,18 @@ func (module *module) discoverModels(ctx context.Context, orgID valuer.UUID) ([]
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{Expression: fmt.Sprintf("%s EXISTS", telemetrytypes.GenAIRequestModel)},
|
||||
Filter: &qbtypes.Filter{Expression: fmt.Sprintf("%s EXISTS", llmpricingruletypes.GenAIRequestModel)},
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{Expression: "count()", Alias: "spanCount"},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: telemetrytypes.GenAIRequestModel,
|
||||
Name: llmpricingruletypes.GenAIRequestModel,
|
||||
FieldContext: telemetrytypes.FieldContextSpan,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: telemetrytypes.GenAIProviderName,
|
||||
Name: llmpricingruletypes.GenAIProviderName,
|
||||
FieldContext: telemetrytypes.FieldContextSpan,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
@@ -254,9 +254,9 @@ func (module *module) discoverModels(ctx context.Context, orgID valuer.UUID) ([]
|
||||
switch c.Type {
|
||||
case qbtypes.ColumnTypeGroup:
|
||||
switch c.Name {
|
||||
case telemetrytypes.GenAIRequestModel:
|
||||
case llmpricingruletypes.GenAIRequestModel:
|
||||
modelIdx = i
|
||||
case telemetrytypes.GenAIProviderName:
|
||||
case llmpricingruletypes.GenAIProviderName:
|
||||
providerIdx = i
|
||||
}
|
||||
case qbtypes.ColumnTypeAggregation:
|
||||
|
||||
@@ -35,7 +35,7 @@ func (c *conditionBuilder) ConditionFor(
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken/search are logs-only functions; reject for rule state history.
|
||||
// has/hasAny/hasAll/hasToken are logs-body-only; reject for rule state history.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -137,8 +137,8 @@ func filterqueryParserInit() {
|
||||
0, 0, 191, 19, 1, 0, 0, 0, 192, 190, 1, 0, 0, 0, 193, 194, 7, 2, 0, 0,
|
||||
194, 21, 1, 0, 0, 0, 195, 196, 7, 3, 0, 0, 196, 197, 5, 1, 0, 0, 197, 198,
|
||||
3, 26, 13, 0, 198, 199, 5, 2, 0, 0, 199, 23, 1, 0, 0, 0, 200, 201, 5, 27,
|
||||
0, 0, 201, 202, 5, 1, 0, 0, 202, 203, 3, 18, 9, 0, 203, 204, 5, 2, 0, 0,
|
||||
204, 25, 1, 0, 0, 0, 205, 210, 3, 28, 14, 0, 206, 207, 5, 5, 0, 0, 207,
|
||||
0, 0, 201, 202, 5, 1, 0, 0, 202, 203, 3, 26, 13, 0, 203, 204, 5, 2, 0,
|
||||
0, 204, 25, 1, 0, 0, 0, 205, 210, 3, 28, 14, 0, 206, 207, 5, 5, 0, 0, 207,
|
||||
209, 3, 28, 14, 0, 208, 206, 1, 0, 0, 0, 209, 212, 1, 0, 0, 0, 210, 208,
|
||||
1, 0, 0, 0, 210, 211, 1, 0, 0, 0, 211, 27, 1, 0, 0, 0, 212, 210, 1, 0,
|
||||
0, 0, 213, 217, 3, 34, 17, 0, 214, 217, 3, 32, 16, 0, 215, 217, 3, 30,
|
||||
@@ -2945,7 +2945,7 @@ type ISearchCallContext interface {
|
||||
// Getter signatures
|
||||
SEARCH() antlr.TerminalNode
|
||||
LPAREN() antlr.TerminalNode
|
||||
ValueList() IValueListContext
|
||||
FunctionParamList() IFunctionParamListContext
|
||||
RPAREN() antlr.TerminalNode
|
||||
|
||||
// IsSearchCallContext differentiates from other interfaces.
|
||||
@@ -2992,10 +2992,10 @@ func (s *SearchCallContext) LPAREN() antlr.TerminalNode {
|
||||
return s.GetToken(FilterQueryParserLPAREN, 0)
|
||||
}
|
||||
|
||||
func (s *SearchCallContext) ValueList() IValueListContext {
|
||||
func (s *SearchCallContext) FunctionParamList() IFunctionParamListContext {
|
||||
var t antlr.RuleContext
|
||||
for _, ctx := range s.GetChildren() {
|
||||
if _, ok := ctx.(IValueListContext); ok {
|
||||
if _, ok := ctx.(IFunctionParamListContext); ok {
|
||||
t = ctx.(antlr.RuleContext)
|
||||
break
|
||||
}
|
||||
@@ -3005,7 +3005,7 @@ func (s *SearchCallContext) ValueList() IValueListContext {
|
||||
return nil
|
||||
}
|
||||
|
||||
return t.(IValueListContext)
|
||||
return t.(IFunctionParamListContext)
|
||||
}
|
||||
|
||||
func (s *SearchCallContext) RPAREN() antlr.TerminalNode {
|
||||
@@ -3064,7 +3064,7 @@ func (p *FilterQueryParser) SearchCall() (localctx ISearchCallContext) {
|
||||
}
|
||||
{
|
||||
p.SetState(202)
|
||||
p.ValueList()
|
||||
p.FunctionParamList()
|
||||
}
|
||||
{
|
||||
p.SetState(203)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user