Compare commits

...

18 Commits

Author SHA1 Message Date
nityanandagohain
decf6760d3 fix: update integration tests 2026-08-05 12:21:27 +05:30
nityanandagohain
1e20dc7a87 fix: remove if condition for json parser 2026-08-05 11:57:07 +05:30
Ashwin Bhatkal
a6ac14344e fix(dashboard): count panel stats from the v2 spec (#12396)
* fix(dashboard): count panel stats from the v2 spec

The panel counters walked a top-level `widgets` array and returned early
when the key was missing. A v2 dashboard stores only `metadata` and
`spec`, with panels as a map under `spec.panels`, so every v2 row hit
that early return and all `dashboard.panels.*` stats stayed at zero.
`dashboard.count` was unaffected — it is a row count.

Read the v2 spec instead: count the panels under `spec.panels` and take
each panel's signal from its query envelope, reusing the typed read path
and `QueryEnvelope.GetSignal`. Signal-less queries (promql, clickhouse
sql, formulas) count towards the panel total only. v1 rows are no longer
parsed for panel stats and contribute to `dashboard.count` alone.

* test(dashboard): drop the constant name arg from the stats query helper

statsBuilderQuery only ever received "A", which go-lint flags via unparam.
A panel holds a single query, so the name never mattered to the assertions;
the composite test still names its sub-queries through statsBuilderQuerySpec.

* refactor(dashboard): move v2 panel stats to a perses_ file

All v2 code lives in perses_-prefixed files until the v1 code goes away.
Pure move of the stats block out of dashboard.go, tests alongside it.

* fix(dashboard): count create-v2 stats off the postable spec

CreateV2 already holds the postable dashboard, so decoding the storable
back into a v2 dashboard just to count its panels was a needless type
conversion on the create path.

Split the panel walk into addPanelStats over a DashboardSpec, and add
NewStatsFromPostableDashboardV2 for the create path. The storable variant
keeps its signature for the periodic collectors, which only have rows.
2026-08-05 04:16:02 +00:00
Vinicius Lourenço
e4e1c4b9e0 refactor(infrastructure-monitoring-v2): more ux/ui fixes after bug-bash (#12402)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix(k8s-expanded-row): ensure values are correctly aligned

Ref: https://app.notion.com/p/signoz/Alignment-in-rows-inside-a-group-wrt-column-headers-is-not-correct-3b1fcc6bcd1980dd910bff84d1defe47?v=65cfcc6bcd1982a6bee688d8fd55420c&source=copy_link

* fix(pods): ensure table aligment on group by

Ref: https://app.notion.com/p/signoz/Alignment-in-rows-inside-a-group-wrt-column-headers-is-not-correct-3b1fcc6bcd1980dd910bff84d1defe47?v=65cfcc6bcd1982a6bee688d8fd55420c&source=copy_link

* fix(infra-monitoring): not allowing name columns to be sorted

Ref: https://app.notion.com/p/signoz/No-option-to-order-resources-by-name-3b1fcc6bcd198019a729d2575731387e?v=65cfcc6bcd1982a6bee688d8fd55420c&source=copy_link

* fix(infra-monitoring): cannot sort name columns when group by is enabled

Ref: https://app.notion.com/p/signoz/Cannot-use-sort-when-group-by-is-added-3b2fcc6bcd19806d9514d27e4da26c35?v=65cfcc6bcd1982a6bee688d8fd55420c&source=copy_link

* fix(infra-monitoring): sort key was not correct

Ref: https://app.notion.com/p/signoz/Review-all-sortable-columns-3b2fcc6bcd1980c4b267f3e35d4e875f?v=65cfcc6bcd1982a6bee688d8fd55420c&source=copy_link

* feat(infra-monitoring): add descriptions for each threshold of entity progress

Ref: https://app.notion.com/p/signoz/For-usage-metrics-the-info-icon-can-explain-the-colours-right-there-instead-of-just-saying-to-go-to-3b1fcc6bcd1980fc81a4f2b8179541b9?v=65cfcc6bcd1982a6bee688d8fd55420c&source=copy_link

* fix(infra-monitoring): explorer button not allowing the back button

Ref: https://app.notion.com/p/signoz/After-click-to-go-to-metrics-explorer-on-chart-we-cannot-go-back-3b1fcc6bcd1980d7b52fd7dbe0b3e29f?v=65cfcc6bcd1982a6bee688d8fd55420c&source=copy_link

* fix(infra-monitoring): polluted navigation history

Fixes the following issue:
- Go to namespaces
- Select “generator” and open the details
- Close the details
- Go to pods
- Group by namespaces

Expand “generator”, this will cause it to be redirected to namespaces category

Ref: https://app.notion.com/p/signoz/wrong-redirect-when-navigating-between-categories-3b2fcc6bcd1980e996fedd639ce72b01?v=65cfcc6bcd1982a6bee688d8fd55420c&source=copy_link

* fix(infra-monitoring): get min max not detecting 1month as valid interval

Ref: https://app.notion.com/p/signoz/1month-is-not-supported-to-be-received-as-relativeTime-on-metrics-explorer-3b2fcc6bcd1980c0bd17f1e3d869e987?v=65cfcc6bcd1982a6bee688d8fd55420c&source=copy_link

* refactor(infra-monitoring): update table statistics for better auto page size

Ref: https://app.notion.com/p/signoz/change-the-baseline-values-for-calculated-page-size-3b2fcc6bcd19807caafbdedb725b6ab7?v=65cfcc6bcd1982a6bee688d8fd55420c&source=copy_link
2026-08-04 19:44:04 +00:00
Nityananda Gohain
816ae7760e feat: QB support for llm trace list and span list (#12027)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat: support llm trace list and span list

* fix: take perf into consideration

* fix: more tests

* fix: more cleanup

* fix: cleanup and more tests

* fix: add resource fingerprint cte

* fix: edge cases and correct cost key

* fix: update integration test

* fix: update openapi

* fix: address comments

* fix: address comments

* fix: fix tests

* fix: add back the flag in metadata

* fix: remove source and change to builder ai query

* fix: updated openapi

* fix: minor cleanup

* fix: refactor as requested

* fix: address comments

* fix: address comments

* fix: remove tracefield. explicit rejection

* fix: remove comment

* fix: remove accidentally added file

* fix: add NewFactory

* fix: refactor integration tests

* fix: address comment

* fix: use assert

* fix: use assert and condense comments
2026-08-04 16:44:43 +00:00
Nageshbansal
985523df93 fix(version): detect docker under private cgroup namespaces (#12397)
* fix(version): detect docker under private cgroup namespaces

Docker defaults containers to a private cgroup namespace on cgroup v2
hosts (20.10+), which remaps /proc/self/cgroup to "0::/" and defeats
the runtime-name sniff, so compose installs report
deployment.mode=unknown. Check the container manager marker files
(/run/.containerenv, /.dockerenv) and the container env var first,
mirroring systemd's detect_container, and fall back to mountinfo when
the cgroup path is namespaced away.

* fix(version): drop the docker case from the container env check

docker never sets the container variable; /.dockerenv already covers it
2026-08-04 16:40:46 +00:00
Nageshbansal
5c9606bf9b feat(version): detect more cloud platforms (#12403)
Add metadata endpoint checks for openstack (covers OVHcloud), oracle,
alibaba, akamai (linode), ibm, and scaleway, and the FLY_APP_NAME
environment variable for fly.io. OpenStack must come before AWS since
it exposes an EC2-compatible endpoint.
2026-08-04 16:39:03 +00:00
Vikrant Gupta
f9461415bb chore(flagger): remove use_fine_grained_authz feature flag (#12400) 2026-08-04 16:27:28 +00:00
Tushar Vats
d93713c184 fix(querybuilder): keep DateTime64 columns native in aggregations (#12382)
A numeric coercion yields seconds since epoch, so max(timestamp) came back
as 1758113657.04 and the third-party APIs "Last Seen" rendered as January
1970. Both rules key on the physical column type, since no FieldDataType
denotes a timestamp.

- a time column is never coerced: it reaches the aggregate in its native
  type and the driver returns a time.Time
- a bare `timestamp` resolves to the intrinsic column alone; a same-named
  numeric attribute no longer joins the candidate union, where the mixed
  branches failed with "no supertype for DateTime64, Float64"
- the exists guard leaves the String bucket: `timestamp <> ''` becomes a
  typed epoch-zero comparison, verified equivalent on ClickHouse 25.5
  including a non-UTC server timezone
- max/min/quantile/count keep working; sum/avg and the rate_* variants now
  fail at the database instead of returning a rescaled number, pinned by
  integration tests until they are rejected up front
- tests/fixtures/traces.py wrote kind_string as the enum member name
  ("SPAN_KIND_CLIENT"), so any feature filtering on it matched nothing; it
  now maps the six kinds to the exporter's form ("Client")
- Last Seen assertions pin the encoding and the exact instant, rather than
  accepting either an RFC 3339 string or epoch millis
- drop the /overview/domain integration tests and the fixture surface that
  served only that route; the endpoint is no longer used
2026-08-04 15:07:48 +00:00
Vinicius Lourenço
0b5d3e939f chore(infrastructure-monitoring-v2): remove feature flag (#12398)
* Revert "chore: added infraMonitoringV2 feature flag (#12031)"

This reverts commit 1d441b63cf.

* chore(infrastructure-monitoring-v2): remove feature flag
2026-08-04 13:23:56 +00:00
Naman Verma
089f9eb4c6 chore: add api to retry migration for a dashboard (#12387)
* chore: add api to retry migration for a dashboard

* feat(dashboard): add retry migration action for legacy dashboards

The legacy-dashboard dialog only offered the dashboard ID and a link to
support. Now that the v1->v2 migration can be re-run on demand, let an
editor trigger it from there and fall back to support only if it still
fails.

Retrying needs edit access (the endpoint is EDITOR-gated), so viewers
keep the ID-and-support dialog unchanged.

---------

Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
2026-08-04 13:16:43 +00:00
Vinicius Lourenço
6c659e3b26 refactor(infrastructure-monitoring-v2): ensure pods docs redirect to right place & move events out of constants (#12359)
* fix(pods): redirecting to the wrong doc for chart

* refactor(events): move out of src/constants

* test(use-log): fix issue with mock

* docs(infra-monitoring): fix more broken doc path

* fix(pods): disable sort for pod restarts
2026-08-04 13:07:05 +00:00
Ashwin Bhatkal
c70866e4b3 fix(dashboards-list): never replace past the caret in DSL autocomplete (#12384)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
getCaretContext resolved the active slot to a token's start index while
keeping replaceEnd at the caret, so a caret parked in the whitespace
before that token produced an inverted range (replaceStart > replaceEnd).
dslCompletionSource passes that range to CodeMirror as CompletionResult
from/to, and accepting a suggestion threw

  RangeError: Invalid change range 16 to 15 (in doc of length 36)

inside view.dispatch. The same inverted range made spliceAtCaret
duplicate the skipped character.

The replaced range is defined as ending at the caret, so clamp it there
and let the slot collapse to a plain insertion point.
2026-08-04 10:01:20 +00:00
Swapnil Nakade
34041be308 fix: adjust aggregation values for GCP metrics (#12391) 2026-08-04 09:41:14 +00:00
Srikanth Chekuri
957580ec7c fix(alertmanager): resolve email SMTP settings from env (#12281)
* fix(alertmanager): resolve email SMTP settings from env

* refactor(alertmanager): remove dead legacy config and tidy integration test helpers

* test(alertmanager): scope smtp rotation test to env rotation only

* chore: drop redundant test

* chore: log and warn; do not persist global config
2026-08-04 08:53:34 +00:00
Tushar Vats
77c1b601be feat(logs): implement search() across all log fields (#12244)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
- Replace the search() stub: the logs condition builder fans a case-insensitive
  match of the term across every searchable column (log columns, body/body_v2,
  attribute + resource maps).
- Grammar: searchCall takes a valueList (parser regenerated), so the scoped form
  search('term', body, resource) parses and narrows the fan-out to those contexts.
- The visitor emits FilterOperatorSearch and flags the statement scan-heavy; the
  statement builder attaches a CostGuard the querier enforces via EXPLAIN ESTIMATE
  against a per-shard budget, cumulative across buckets on the window-list path.
- body_v2 gets its own, lower budget (search_max_scan_rows_json_body): toString()
  rebuilds every document and no skip index prunes.
- Logs-only: other signals reject search().
- Unit tests for the per-context fan-out SQL and both budgets; integration suites
  run the same matrix over the legacy body and over body_v2.
2026-08-03 19:03:38 +00:00
Naman Verma
cfaa7de165 fix: enforce the required tag on dashboard spec fields (#12381)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix: enforace the required tag on dashboard spec fields

* test: add empty list and objects for required fields in integration tests
2026-08-03 18:22:43 +00:00
Naman Verma
5b3cc2400f chore: delta temporality metrics should always be considered as a sum metric (#12313)
* chore: delta temporality metrics should always be considered as a sum metric

* test: add integration tests

* test: parametrise the non-reduced test for rate and increase both

* test: add comment explaining last samples in test

* chore: remove unneeded comments

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-08-03 16:08:45 +00:00
209 changed files with 10871 additions and 962 deletions

View File

@@ -39,6 +39,8 @@ jobs:
matrix:
suite:
- alerts
- alertmanager
- alertmanagerrotation
- basepath
- callbackauthn
- cloudintegrations
@@ -53,6 +55,7 @@ jobs:
- queriermetrics
- querierscalar
- queriercommon
- querierai
- rawexportdata
- promqlconformance
- querierauthz

View File

@@ -6902,6 +6902,7 @@ 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'
@@ -6910,6 +6911,7 @@ components:
propertyName: type
oneOf:
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilder'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilderAI'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeFormula'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeTraceOperator'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopePromQL'
@@ -6924,6 +6926,15 @@ components:
required:
- type
type: object
Querybuildertypesv5QueryEnvelopeBuilderAI:
properties:
spec:
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
required:
- type
type: object
Querybuildertypesv5QueryEnvelopeClickHouseSQL:
properties:
spec:
@@ -7037,6 +7048,7 @@ components:
Querybuildertypesv5QueryType:
enum:
- builder_query
- builder_ai_query
- builder_formula
- builder_trace_operator
- clickhouse_sql
@@ -15477,6 +15489,72 @@ 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

View File

@@ -276,6 +276,10 @@ 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)
}

View File

@@ -80,15 +80,6 @@ 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()),
@@ -107,15 +98,6 @@ 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 {

View File

@@ -52,6 +52,8 @@ import type {
ListDashboardsV2200,
ListDashboardsV2Params,
LockDashboardV2PathParameters,
MigrateDashboardV2200,
MigrateDashboardV2PathParameters,
PatchDashboardV2200,
PatchDashboardV2PathParameters,
PinDashboardV2PathParameters,
@@ -1804,6 +1806,85 @@ 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)

View File

@@ -4301,6 +4301,18 @@ 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
@@ -4484,6 +4496,7 @@ export interface Querybuildertypesv5QueryEnvelopeClickHouseSQLDTO {
export type Querybuildertypesv5QueryEnvelopeDTO =
| Querybuildertypesv5QueryEnvelopeBuilderDTO
| Querybuildertypesv5QueryEnvelopeBuilderAIDTO
| Querybuildertypesv5QueryEnvelopeFormulaDTO
| Querybuildertypesv5QueryEnvelopeTraceOperatorDTO
| Querybuildertypesv5QueryEnvelopePromQLDTO
@@ -8287,6 +8300,7 @@ 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',
@@ -11164,6 +11178,17 @@ 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

View File

@@ -1,9 +1,3 @@
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',
@@ -45,155 +39,3 @@ 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,
});
}

View File

@@ -9,8 +9,6 @@ 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',
}

View File

@@ -1,5 +1,8 @@
// ** Helpers
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import {
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
@@ -389,11 +392,17 @@ const METRIC_TYPE_TO_ATTRIBUTE_TYPE: Record<
export function toAttributeType(
metricType: MetrictypesTypeDTO | undefined,
isMonotonic?: boolean,
temporality?: MetrictypesTemporalityDTO,
): ATTRIBUTE_TYPES | '' {
if (!metricType) {
return '';
}
if (metricType === MetrictypesTypeDTO.sum && isMonotonic === false) {
// Only non-monotonic cumulative sums are treated as gauges; delta sums stay Sum
if (
metricType === MetrictypesTypeDTO.sum &&
isMonotonic === false &&
temporality === MetrictypesTemporalityDTO.cumulative
) {
return ATTRIBUTE_TYPES.GAUGE;
}
return METRIC_TYPE_TO_ATTRIBUTE_TYPE[metricType] || '';

View File

@@ -17,11 +17,7 @@ import {
QuickFilterChangeEventData,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import {
InfraMonitoringEvents,
logInfraFilterCustomizedEvent,
logInfraMonitoringListViewedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import { initialQueriesMap } from 'constants/queryBuilder';
import K8sBaseDetails, {
K8sDetailsFilters,
@@ -57,6 +53,10 @@ 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);

View File

@@ -1,5 +1,4 @@
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { logInfraFilterCustomizedEvent } from 'constants/events';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import {
StatusFilterValue,
@@ -9,6 +8,7 @@ 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;

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Badge } from '@signozhq/ui/badge';
import { Progress } from '@signozhq/ui/progress';
import {
@@ -10,6 +9,7 @@ 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,26 +18,6 @@ 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[] = [
@@ -79,7 +59,7 @@ export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
render: (value): React.ReactNode => (
<Progress
percent={Number(Number(value).toFixed(1))}
strokeColor={getProgressColor(Number(value))}
strokeColor={getStrokeColorForPercent('cpu', Number(value))}
showInfo
/>
),
@@ -90,7 +70,7 @@ export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
render: (value): React.ReactNode => (
<Progress
percent={Number(Number(value).toFixed(1))}
strokeColor={getMemoryProgressColor(Number(value))}
strokeColor={getStrokeColorForPercent('memory', Number(value))}
showInfo
/>
),

View File

@@ -9,6 +9,7 @@ import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { getGroupByEl } from 'container/InfraMonitoringK8sV2/Base/utils';
import {
EntityProgressBar,
EntityProgressThresholds,
ExpandButtonWrapper,
GroupedStatusCounts,
ValidateColumnValueWrapper,
@@ -98,7 +99,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
),
},
{
id: 'hostName',
id: INFRA_MONITORING_ATTR_KEYS.HOST_NAME,
header: (): React.ReactNode => (
<EntityGroupHeader
title="Hostname"
@@ -108,7 +109,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
),
accessorFn: (row): string => row.hostName ?? '',
width: { min: 290 },
enableSort: false,
enableSort: true,
enableRemove: false,
enableMove: false,
pin: 'left',
@@ -168,7 +169,10 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
{
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#cpu-usage">
<ColumnHeader
docPath="/infrastructure-monitoring/host-monitoring#cpu-usage"
tooltip={<EntityProgressThresholds type="cpu" />}
>
CPU Usage
</ColumnHeader>
),
@@ -195,7 +199,9 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader
tooltip="Excluding cache memory."
tooltip={
<EntityProgressThresholds type="memory" note="Excluding cache memory." />
}
docPath="/infrastructure-monitoring/host-monitoring#memory-usage"
>
Memory Usage (WSS)
@@ -221,9 +227,12 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
},
},
{
id: 'diskUsage',
id: 'disk_usage',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#disk-usage">
<ColumnHeader
docPath="/infrastructure-monitoring/host-monitoring#disk-usage"
tooltip={<EntityProgressThresholds type="disk" />}
>
Disk Usage
</ColumnHeader>
),

View File

@@ -3,13 +3,14 @@ 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?: string;
tooltip?: React.ReactNode;
className?: string;
}
@@ -19,6 +20,9 @@ function ColumnHeader({
tooltip,
className,
}: ColumnHeaderProps): JSX.Element {
const stopPropagationHandler: MouseEventHandler = (e): void =>
e.stopPropagation();
const renderContent = (): React.ReactNode => {
if (children) {
return children;
@@ -30,21 +34,25 @@ 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={(e): void => e.stopPropagation()}
onClick={stopPropagationHandler}
>
Learn more.
{isJustStringTitle
? 'Learn more.'
: 'Check the documentation to learn more.'}
</a>
</>
</div>
}
>
<div className={styles.infoIcon}>
@@ -56,7 +64,9 @@ function ColumnHeader({
if (tooltip) {
return (
<TooltipSimple title={tooltip}>
<TooltipSimple
title={<div onClick={stopPropagationHandler}>{tooltip}</div>}
>
<div className={styles.infoIcon}>
<Info size="md" />
</div>

View File

@@ -12,11 +12,7 @@ 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,
logInfraDrawerTabViewedEvent,
logInfraExplorerNavigatedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
@@ -50,6 +46,8 @@ 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>({

View File

@@ -9,11 +9,7 @@ import TanStackTable, {
useHiddenColumnIds,
useTableParams,
} from 'components/TanStackTableView';
import {
InfraMonitoringEvents,
logInfraColumnSortedEvent,
logInfraTimeRangeCustomizedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
@@ -48,6 +44,10 @@ 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,6 +128,8 @@ export function K8sBaseList<
const { containerRef, calculatedPageSize } = useCalculatedPageSize({
rowHeight: 42,
headerHeight: 58,
paginationHeight: 52,
});
const {
@@ -436,16 +438,17 @@ 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'}

View File

@@ -13,7 +13,7 @@
--tanstack-table-resize-handle-hover-bg: var(--l1-border);
--tanstack-table-row-height: 36px;
--tanstack-cell-padding-left-override: 15px;
--tanstack-cell-padding-left-override: 26px;
--tanstack-cell-padding-right-override: 15px;
& [data-hide-expanded='true'] {

View File

@@ -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 { logInfraColumnSortedEvent } from 'constants/events';
import { InfraMonitoringEntity } from '../constants';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
InfraMonitoringEntity,
} from '../constants';
import {
SelectedItemParams,
useInfraMonitoringGroupBy,
@@ -36,6 +36,9 @@ 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;
@@ -92,7 +95,6 @@ export function K8sExpandedRow<
const [, setSelectedItemParams] = useInfraMonitoringSelectedItemParams();
const [, setMainOrderBy] = useInfraMonitoringOrderBy();
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const location = useLocation();
const queryClient = useQueryClient();
@@ -258,13 +260,26 @@ export function K8sExpandedRow<
},
};
const newUrlQuery = new URLSearchParams(urlQuery.toString());
newUrlQuery.set(
const searchParams = getUnstableCurrentSearchParams();
searchParams.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(updatedQuery)),
);
safeNavigate(`${location.pathname}?${newUrlQuery.toString()}`);
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()}`);
};
const total = data?.total ?? 0;
@@ -276,6 +291,7 @@ export function K8sExpandedRow<
color="secondary"
variant="outlined"
className={styles.viewAllButton}
data-testid="expanded-row-view-all"
onClick={handleViewAllClick}
prefix={<CornerDownRight size={14} />}
>

View File

@@ -2,10 +2,7 @@ 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,
logInfraFilterCustomizedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
@@ -24,6 +21,7 @@ import {
import { useInfraMonitoringPageListing } from '../hooks';
import styles from './K8sHeader.module.scss';
import { logInfraFilterCustomizedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
interface K8sHeaderProps {
controlListPrefix?: React.ReactNode;

View File

@@ -4,19 +4,34 @@ import { Select } from 'antd';
import { Download, SlidersVertical } from '@signozhq/icons';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import logEvent from 'api/common/logEvent';
import {
InfraMonitoringEvents,
logInfraGroupByCustomizedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import { InfraMonitoringEntity } from '../constants';
import {
INFRA_MONITORING_ATTR_KEYS,
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;
@@ -37,11 +52,17 @@ 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, {
@@ -52,15 +73,16 @@ function K8sTableToolbar({
logInfraGroupByCustomizedEvent(entity, value);
},
[entity, eventCategory, setCurrentPage, setGroupBy],
[entity, eventCategory, orderBy, setCurrentPage, setOrderBy, setGroupBy],
);
return (
<div className={styles.toolbar}>
<div className={styles.groupByContainer}>
<div className={styles.groupByContainer} data-testid="k8s-table-group-by">
<div className={styles.groupByLabel}>Group by</div>
<Select
className={styles.groupBySelect}
data-testid="k8s-table-group-by-select"
loading={isLoadingGroupByFilters}
mode="multiple"
value={groupBy}

View File

@@ -1370,4 +1370,127 @@ 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);
});
});
});
});

View File

@@ -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('constants/events', () => ({
jest.mock('container/InfraMonitoringK8sV2/Base/events', () => ({
logInfraColumnCustomizedEvent: jest.fn(),
}));

View File

@@ -3,7 +3,6 @@ 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';
@@ -17,6 +16,7 @@ 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;

View File

@@ -0,0 +1,113 @@
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,
});
}

View File

@@ -4,7 +4,6 @@ import {
useColumnOrder,
useHiddenColumnIds,
} from 'components/TanStackTableView';
import { logInfraColumnCustomizedEvent } from 'constants/events';
import { InfraMonitoringEntity } from '../constants';
@@ -13,6 +12,7 @@ import {
useInfraMonitoringLineClamp,
} from './useInfraMonitoringTablePreferencesStore';
import { sortByColumnOrder } from './utils';
import { logInfraColumnCustomizedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
interface UseEmitColumnCustomizedParams<TData> {
entity: InfraMonitoringEntity;

View File

@@ -60,7 +60,7 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
},
},
{
id: 'clusterName',
id: INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME,
header: (): React.ReactNode => (
<EntityGroupHeader
title="Cluster Name"
@@ -70,7 +70,7 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
),
accessorFn: (row): string => row.clusterName || '',
width: { min: 290 },
enableSort: false,
enableSort: true,
enableRemove: false,
enableMove: false,
pin: 'left',

View File

@@ -10,6 +10,7 @@ import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
EntityProgressBar,
EntityProgressThresholds,
GroupedStatusCounts,
TextNoData,
ValidateColumnValueWrapper,
@@ -69,7 +70,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
},
},
{
id: 'daemonsetName',
id: INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME,
header: (): React.ReactNode => (
<EntityGroupHeader
title="DaemonSet Name"
@@ -80,7 +81,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '',
width: { min: 290 },
enableSort: false,
enableSort: true,
enableRemove: false,
enableMove: false,
pin: 'left',
@@ -174,7 +175,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
{
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-req-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-req-usage-"
tooltip={<EntityProgressThresholds type="cpu-request" />}
>
CPU Request Usage (%)
</ColumnHeader>
),
@@ -192,7 +196,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="CPU Request"
>
<EntityProgressBar value={cpuRequest} type="request" />
<EntityProgressBar value={cpuRequest} type="cpu-request" />
</ValidateColumnValueWrapper>
);
},
@@ -200,7 +204,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
{
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-limit-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-limit-usage-"
tooltip={<EntityProgressThresholds type="cpu-limit" />}
>
CPU Limit Usage (%)
</ColumnHeader>
),
@@ -217,7 +224,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="CPU Limit"
>
<EntityProgressBar value={cpuLimit} type="limit" />
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
</ValidateColumnValueWrapper>
);
},
@@ -251,7 +258,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
{
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-req-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-req-usage-"
tooltip={<EntityProgressThresholds type="memory-request" />}
>
Memory Request Usage (%)
</ColumnHeader>
),
@@ -269,7 +279,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="Memory Request"
>
<EntityProgressBar value={memoryRequest} type="request" />
<EntityProgressBar value={memoryRequest} type="memory-request" />
</ValidateColumnValueWrapper>
);
},
@@ -277,7 +287,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
{
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-limit-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-limit-usage-"
tooltip={<EntityProgressThresholds type="memory-limit" />}
>
Memory Limit Usage (%)
</ColumnHeader>
),
@@ -294,7 +307,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="Memory Limit"
>
<EntityProgressBar value={memoryLimit} type="limit" />
<EntityProgressBar value={memoryLimit} type="memory-limit" />
</ValidateColumnValueWrapper>
);
},

View File

@@ -10,6 +10,7 @@ import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
EntityProgressBar,
EntityProgressThresholds,
GroupedStatusCounts,
TextNoData,
ValidateColumnValueWrapper,
@@ -70,7 +71,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
},
},
{
id: 'deploymentName',
id: INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME,
header: (): React.ReactNode => (
<EntityGroupHeader
title="Deployment Name"
@@ -81,7 +82,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '',
width: { min: 290 },
enableSort: false,
enableSort: true,
enableRemove: false,
enableMove: false,
pin: 'left',
@@ -162,7 +163,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
{
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-req-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-req-usage-"
tooltip={<EntityProgressThresholds type="cpu-request" />}
>
CPU Request Usage (%)
</ColumnHeader>
),
@@ -180,7 +184,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU Request"
>
<EntityProgressBar value={cpuRequest} type="request" />
<EntityProgressBar value={cpuRequest} type="cpu-request" />
</ValidateColumnValueWrapper>
);
},
@@ -188,7 +192,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
{
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-limit-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-limit-usage-"
tooltip={<EntityProgressThresholds type="cpu-limit" />}
>
CPU Limit Usage (%)
</ColumnHeader>
),
@@ -205,7 +212,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU Limit"
>
<EntityProgressBar value={cpuLimit} type="limit" />
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
</ValidateColumnValueWrapper>
);
},
@@ -238,7 +245,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
{
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-req-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/deployments#mem-req-usage-"
tooltip={<EntityProgressThresholds type="memory-request" />}
>
Memory Request Usage (%)
</ColumnHeader>
),
@@ -256,7 +266,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="Memory Request"
>
<EntityProgressBar value={memoryRequest} type="request" />
<EntityProgressBar value={memoryRequest} type="memory-request" />
</ValidateColumnValueWrapper>
);
},
@@ -264,7 +274,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
{
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-limit-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/deployments#mem-limit-usage-"
tooltip={<EntityProgressThresholds type="memory-limit" />}
>
Memory Limit Usage (%)
</ColumnHeader>
),
@@ -281,7 +294,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="Memory Limit"
>
<EntityProgressBar value={memoryLimit} type="limit" />
<EntityProgressBar value={memoryLimit} type="memory-limit" />
</ValidateColumnValueWrapper>
);
},

View File

@@ -3,10 +3,7 @@ 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,
logInfraDrawerTimeRangeCustomizedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
@@ -17,6 +14,7 @@ import {
import { useEntityDetailsTime } from './useEntityDetailsTime';
import styles from './EntityDateTimeSelector.module.scss';
import { logInfraDrawerTimeRangeCustomizedEvent } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/events';
interface EntityDateTimeSelectorProps {
eventEntity: string;

View File

@@ -16,10 +16,7 @@ import {
combineInitialAndUserExpression,
getUserExpressionFromCombined,
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import {
InfraMonitoringEvents,
logInfraDrawerFilterCustomizedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import Controls from 'container/Controls';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import LoadingContainer from 'container/InfraMonitoringK8sV2/LoadingContainer';
@@ -41,6 +38,7 @@ 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;

View File

@@ -20,10 +20,7 @@ import {
combineInitialAndUserExpression,
getUserExpressionFromCombined,
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import {
InfraMonitoringEvents,
logInfraDrawerFilterCustomizedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
@@ -53,6 +50,7 @@ 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;

View File

@@ -2,10 +2,7 @@ import { useCallback, useMemo, useRef } from 'react';
import { UseQueryResult } from 'react-query';
import { Skeleton } from 'antd';
import cx from 'classnames';
import {
InfraMonitoringEvents,
logInfraExplorerNavigatedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } 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';
@@ -35,6 +32,7 @@ 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;

View File

@@ -16,10 +16,7 @@ import {
getUserExpressionFromCombined,
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { ResizeTable } from 'components/ResizeTable';
import {
InfraMonitoringEvents,
logInfraDrawerFilterCustomizedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import Controls from 'container/Controls';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
@@ -41,6 +38,7 @@ 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;

View File

@@ -0,0 +1,48 @@
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,
});
}

View File

@@ -51,14 +51,14 @@ import {
} from './hooks';
import styles from './InfraMonitoringK8s.module.scss';
import {
logInfraFilterCustomizedEvent,
logInfraMonitoringListViewedEvent,
InfraMonitoringEvents,
} from 'constants/events';
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';
export default function InfraMonitoringK8s(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);

View File

@@ -10,6 +10,7 @@ import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
EntityProgressBar,
EntityProgressThresholds,
GroupedStatusCounts,
TextNoData,
ValidateColumnValueWrapper,
@@ -63,7 +64,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
},
},
{
id: 'jobName',
id: INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME,
header: (): React.ReactNode => (
<EntityGroupHeader
title="Job Name"
@@ -74,7 +75,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '',
width: { min: 290 },
enableSort: false,
enableSort: true,
enableRemove: false,
enableMove: false,
pin: 'left',
@@ -158,7 +159,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
{
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-req-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-req-usage-"
tooltip={<EntityProgressThresholds type="cpu-request" />}
>
CPU Request Usage (%)
</ColumnHeader>
),
@@ -176,7 +180,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
attribute="CPU Request"
rowId={rowId}
>
<EntityProgressBar value={cpuRequest} type="request" />
<EntityProgressBar value={cpuRequest} type="cpu-request" />
</ValidateColumnValueWrapper>
);
},
@@ -184,7 +188,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
{
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-limit-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-limit-usage-"
tooltip={<EntityProgressThresholds type="cpu-limit" />}
>
CPU Limit Usage (%)
</ColumnHeader>
),
@@ -201,7 +208,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
attribute="CPU Limit"
rowId={rowId}
>
<EntityProgressBar value={cpuLimit} type="limit" />
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
</ValidateColumnValueWrapper>
);
},
@@ -234,7 +241,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
{
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-req-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/jobs#mem-req-usage-"
tooltip={<EntityProgressThresholds type="memory-request" />}
>
Memory Request Usage (%)
</ColumnHeader>
),
@@ -252,7 +262,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
attribute="Memory Request"
rowId={rowId}
>
<EntityProgressBar value={memoryRequest} type="request" />
<EntityProgressBar value={memoryRequest} type="memory-request" />
</ValidateColumnValueWrapper>
);
},
@@ -260,7 +270,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
{
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-limit-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/jobs#mem-limit-usage-"
tooltip={<EntityProgressThresholds type="memory-limit" />}
>
Memory Limit Usage (%)
</ColumnHeader>
),
@@ -277,7 +290,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
attribute="Memory Limit"
rowId={rowId}
>
<EntityProgressBar value={memoryLimit} type="limit" />
<EntityProgressBar value={memoryLimit} type="memory-limit" />
</ValidateColumnValueWrapper>
);
},

View File

@@ -111,7 +111,8 @@ export const namespaceWidgetInfo = [
{
title: 'CPU Usage (cores)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#cpu-usage-cores',
docPath:
'/infrastructure-monitoring/kubernetes/namespaces/#cpu-usage-cores-1',
},
{
title: 'Memory Usage (bytes)',

View File

@@ -66,7 +66,7 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
},
},
{
id: 'namespaceName',
id: INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME,
header: (): React.ReactNode => (
<EntityGroupHeader
title="Namespace Name"
@@ -76,7 +76,7 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
),
accessorFn: (row): string => row.namespaceName || '',
width: { min: 290 },
enableSort: false,
enableSort: true,
enableRemove: false,
enableMove: false,
pin: 'left',

View File

@@ -57,7 +57,7 @@ export const nodeWidgetInfo = [
{
title: 'CPU Usage (cores)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#cpu-usage-cores',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#cpu-usage-cores-1',
},
{
title: 'Memory Usage (bytes)',

View File

@@ -68,7 +68,7 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
},
},
{
id: 'nodeName',
id: INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME,
header: (): React.ReactNode => (
<EntityGroupHeader
title="Node Name"
@@ -78,7 +78,7 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
),
accessorFn: (row): string => row.nodeName || '',
width: { min: 290 },
enableSort: false,
enableSort: true,
enableRemove: false,
enableMove: false,
pin: 'left',

View File

@@ -67,7 +67,7 @@ export const podWidgetInfo = [
{
title: 'CPU Usage (cores)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/pods/#cpu-usage-cores',
docPath: '/infrastructure-monitoring/kubernetes/pods/#cpu-usage-cores-1',
},
{
title: 'CPU Request, Limit Utilization',

View File

@@ -17,6 +17,7 @@ import {
} from '../commonUtils';
import {
EntityProgressBar,
EntityProgressThresholds,
GroupedStatusCounts,
TextNoData,
ValidateColumnValueWrapper,
@@ -68,7 +69,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
},
{
id: 'podName',
id: INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME,
header: (): React.ReactNode => (
<EntityGroupHeader
title="Pod Name"
@@ -79,7 +80,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
width: { min: 290 },
enableSort: false,
enableSort: true,
enableRemove: false,
enableMove: false,
pin: 'left',
@@ -96,7 +97,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
</ColumnHeader>
),
accessorFn: (row): string => row.podStatus,
width: { min: 160 },
width: { min: 250 },
enableSort: false,
visibilityBehavior: 'hidden-on-expand',
cell: ({ row }): React.ReactNode => {
@@ -175,7 +176,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
),
accessorFn: (row): number => row.podRestarts,
width: { min: 140 },
enableSort: true,
enableSort: false,
cell: ({ value, rowId }): React.ReactNode => {
const restarts = value as number;
return (
@@ -193,7 +194,10 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
{
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-req-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/pods#cpu-req-usage-"
tooltip={<EntityProgressThresholds type="cpu-request" />}
>
CPU Request Usage (%)
</ColumnHeader>
),
@@ -210,7 +214,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
entity={InfraMonitoringEntity.PODS}
attribute="CPU Request"
>
<EntityProgressBar value={cpuRequest} type="request" />
<EntityProgressBar value={cpuRequest} type="cpu-request" />
</ValidateColumnValueWrapper>
);
},
@@ -218,7 +222,10 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
{
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-limit-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/pods#cpu-limit-usage-"
tooltip={<EntityProgressThresholds type="cpu-limit" />}
>
CPU Limit Usage (%)
</ColumnHeader>
),
@@ -234,7 +241,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
entity={InfraMonitoringEntity.PODS}
attribute="CPU Limit"
>
<EntityProgressBar value={cpuLimit} type="limit" />
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
</ValidateColumnValueWrapper>
);
},
@@ -266,7 +273,10 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
{
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-req-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/pods#mem-req-usage-"
tooltip={<EntityProgressThresholds type="memory-request" />}
>
Memory Request Usage (%)
</ColumnHeader>
),
@@ -283,7 +293,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
entity={InfraMonitoringEntity.PODS}
attribute="Memory Request"
>
<EntityProgressBar value={memoryRequest} type="request" />
<EntityProgressBar value={memoryRequest} type="memory-request" />
</ValidateColumnValueWrapper>
);
},
@@ -291,7 +301,10 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
{
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-limit-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/pods#mem-limit-usage-"
tooltip={<EntityProgressThresholds type="memory-limit" />}
>
Memory Limit Usage (%)
</ColumnHeader>
),
@@ -307,7 +320,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
entity={InfraMonitoringEntity.PODS}
attribute="Memory Limit"
>
<EntityProgressBar value={memoryLimit} type="limit" />
<EntityProgressBar value={memoryLimit} type="memory-limit" />
</ValidateColumnValueWrapper>
);
},

View File

@@ -10,6 +10,7 @@ import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
EntityProgressBar,
EntityProgressThresholds,
GroupedStatusCounts,
TextNoData,
ValidateColumnValueWrapper,
@@ -70,7 +71,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
},
},
{
id: 'statefulsetName',
id: INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME,
header: (): React.ReactNode => (
<EntityGroupHeader
title="StatefulSet Name"
@@ -81,7 +82,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] || '',
width: { min: 290 },
enableSort: false,
enableSort: true,
enableRemove: false,
enableMove: false,
pin: 'left',
@@ -165,7 +166,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
{
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-req-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-req-usage-"
tooltip={<EntityProgressThresholds type="cpu-request" />}
>
CPU Request Usage (%)
</ColumnHeader>
),
@@ -183,7 +187,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="CPU Request"
>
<EntityProgressBar value={cpuRequest} type="request" />
<EntityProgressBar value={cpuRequest} type="cpu-request" />
</ValidateColumnValueWrapper>
);
},
@@ -191,7 +195,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
{
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-limit-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-limit-usage-"
tooltip={<EntityProgressThresholds type="cpu-limit" />}
>
CPU Limit Usage (%)
</ColumnHeader>
),
@@ -208,7 +215,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="CPU Limit"
>
<EntityProgressBar value={cpuLimit} type="limit" />
<EntityProgressBar value={cpuLimit} type="cpu-limit" />
</ValidateColumnValueWrapper>
);
},
@@ -242,7 +249,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
{
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-req-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-req-usage-"
tooltip={<EntityProgressThresholds type="memory-request" />}
>
Memory Request Usage (%)
</ColumnHeader>
),
@@ -260,7 +270,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="Memory Request"
>
<EntityProgressBar value={memoryRequest} type="request" />
<EntityProgressBar value={memoryRequest} type="memory-request" />
</ValidateColumnValueWrapper>
);
},
@@ -268,7 +278,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
{
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-limit-usage-">
<ColumnHeader
docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-limit-usage-"
tooltip={<EntityProgressThresholds type="memory-limit" />}
>
Memory Limit Usage (%)
</ColumnHeader>
),
@@ -285,7 +298,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="Memory Limit"
>
<EntityProgressBar value={memoryLimit} type="limit" />
<EntityProgressBar value={memoryLimit} type="memory-limit" />
</ValidateColumnValueWrapper>
);
},

View File

@@ -64,7 +64,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
},
},
{
id: 'pvcName',
id: INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME,
header: (): React.ReactNode => (
<EntityGroupHeader
title="PVC Name"
@@ -74,7 +74,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
),
accessorFn: (row): string => row.persistentVolumeClaimName || '',
width: { min: 290 },
enableSort: false,
enableSort: true,
enableRemove: false,
enableMove: false,
pin: 'left',
@@ -195,7 +195,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
},
},
{
id: 'inodesUsed',
id: 'inodes_used',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes-used">
Inodes Used
@@ -219,7 +219,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
},
},
{
id: 'inodesFree',
id: 'inodes_free',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes-free">
Inodes Free

View File

@@ -26,48 +26,6 @@ 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

View File

@@ -1,35 +1,11 @@
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';
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);
}
}
import {
EntityProgressBarType,
getStrokeColor,
} from './EntityProgressBar.utils';
export function EntityProgressBar({
value,

View File

@@ -0,0 +1,254 @@
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)));
}

View File

@@ -0,0 +1,39 @@
.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;
}

View File

@@ -0,0 +1,56 @@
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>
);
}

View File

@@ -0,0 +1,28 @@
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();
});
});

View File

@@ -1,4 +1,5 @@
export { EntityProgressBar } from './EntityProgressBar';
export { EntityProgressThresholds } from './EntityProgressThresholds';
export { ValidateColumnValueWrapper } from './ValidateColumnValueWrapper';
export { ExpandButtonWrapper } from './ExpandButtonWrapper';
export {

View File

@@ -33,6 +33,7 @@ function AllAttributes({
metricName,
metricType,
isMonotonic,
temporality,
minTime,
maxTime,
}: AllAttributesProps): JSX.Element {
@@ -71,6 +72,7 @@ function AllAttributes({
groupBy,
limit,
isMonotonic,
temporality,
);
handleExplorerTabChange(
PANEL_TYPES.TIME_SERIES,
@@ -89,7 +91,7 @@ function AllAttributes({
[MetricsExplorerEventKeys.AttributeKey]: groupBy,
});
},
[metricName, metricType, isMonotonic, handleExplorerTabChange],
[metricName, metricType, isMonotonic, temporality, handleExplorerTabChange],
);
const goToMetricsExploreWithAppliedAttribute = useCallback(
@@ -101,6 +103,7 @@ function AllAttributes({
undefined,
undefined,
isMonotonic,
temporality,
);
handleExplorerTabChange(
PANEL_TYPES.TIME_SERIES,
@@ -120,7 +123,7 @@ function AllAttributes({
[MetricsExplorerEventKeys.AttributeValue]: value,
});
},
[metricName, metricType, isMonotonic, handleExplorerTabChange],
[metricName, metricType, isMonotonic, temporality, handleExplorerTabChange],
);
const handleKeyMenuItemClick = useCallback(

View File

@@ -86,6 +86,7 @@ function MetricDetails({
undefined,
undefined,
metadata?.isMonotonic,
metadata?.temporality,
);
handleExplorerTabChange(
PANEL_TYPES.TIME_SERIES,
@@ -108,6 +109,7 @@ function MetricDetails({
handleExplorerTabChange,
metadata?.type,
metadata?.isMonotonic,
metadata?.temporality,
]);
useEffect(() => {
@@ -196,6 +198,7 @@ function MetricDetails({
metricName={metricName}
metricType={metadata?.type}
isMonotonic={metadata?.isMonotonic}
temporality={metadata?.temporality}
minTime={minTime}
maxTime={maxTime}
/>

View File

@@ -147,6 +147,44 @@ 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,

View File

@@ -35,6 +35,7 @@ export interface AllAttributesProps {
metricName: string;
metricType: MetrictypesTypeDTO | undefined;
isMonotonic?: boolean;
temporality?: MetrictypesTemporalityDTO;
minTime?: number;
maxTime?: number;
}

View File

@@ -89,12 +89,16 @@ 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;
metricType === MetrictypesTypeDTO.sum &&
isMonotonic === false &&
temporality === MetrictypesTemporalityDTO.cumulative;
switch (metricType) {
case MetrictypesTypeDTO.sum:
@@ -131,7 +135,7 @@ export function getMetricDetailsQuery(
break;
}
const attributeType = toAttributeType(metricType, isMonotonic);
const attributeType = toAttributeType(metricType, isMonotonic, temporality);
return {
...initialQueriesMap[DataSource.METRICS],

View File

@@ -393,12 +393,13 @@ describe('selecting a metric type updates the aggregation options', () => {
]);
});
it('non-monotonic Sum metric is treated as Gauge', () => {
it('cumulative non-monotonic Sum metric is treated as Gauge', () => {
returnMetrics([
makeMetric({
metricName: 'active_connections',
type: MetrictypesTypeDTO.sum,
isMonotonic: false,
temporality: 'cumulative' as never,
}),
]);
@@ -427,6 +428,36 @@ 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 P50P99 space options', () => {
returnMetrics([
makeMetric({

View File

@@ -34,7 +34,7 @@ export type MetricNameSelectorProps = {
function getAttributeType(
metric: MetricsexplorertypesListMetricDTO,
): ATTRIBUTE_TYPES | '' {
return toAttributeType(metric.type, metric.isMonotonic);
return toAttributeType(metric.type, metric.isMonotonic, metric.temporality);
}
function createAutocompleteData(

View File

@@ -1,8 +1,7 @@
import { Route, Switch } from 'react-router-dom';
import ROUTES from 'constants/routes';
import { FeatureKeys } from 'constants/features';
import { server } from 'mocks-server/server';
import { defaultFeatureFlags, render, screen } from 'tests/test-utils';
import { render, screen } from 'tests/test-utils';
import {
invalidLicense,
setupAuthzAdmin,
@@ -59,23 +58,6 @@ 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 });
@@ -113,23 +95,6 @@ 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 });

View File

@@ -1,7 +1,6 @@
import * as roleApi from 'api/generated/services/role';
import { FeatureKeys } from 'constants/features';
import { server } from 'mocks-server/server';
import { defaultFeatureFlags, render, screen, waitFor } from 'tests/test-utils';
import { render, screen, waitFor } from 'tests/test-utils';
import {
invalidLicense,
setupAuthzAdmin,
@@ -33,26 +32,6 @@ 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),

View File

@@ -4,13 +4,7 @@ import {
} from 'mocks-server/__mockdata__/roles';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import {
defaultFeatureFlags,
render,
screen,
userEvent,
} from 'tests/test-utils';
import { FeatureKeys } from 'constants/features';
import { render, screen, userEvent } from 'tests/test-utils';
import {
invalidLicense,
setupAuthzAdmin,
@@ -191,30 +185,6 @@ 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 },

View File

@@ -2,10 +2,15 @@
* 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. This causes the nuqs/react-router-dom to be out of sync.
* 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.
*
* We can revert this commit once we migrate react-router-dom to v6, or once we migrate
* to DateTimeSelectionV3
* 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.
*/
/**

View File

@@ -1,11 +0,0 @@
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,
);
}

View File

@@ -1,4 +1,3 @@
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import { LicenseStatus } from 'types/api/licensesV3/getActive';
@@ -6,22 +5,12 @@ export const useRolesFeatureGate = (): {
isRolesEnabled: boolean;
isLoading: boolean;
} => {
const {
activeLicense,
featureFlags,
isFetchingActiveLicense,
isFetchingFeatureFlags,
} = useAppContext();
const { activeLicense, isFetchingActiveLicense } = useAppContext();
const isValidLicense = activeLicense?.status === LicenseStatus.VALID;
const isFineGrainedAuthzEnabled =
featureFlags?.find((f) => f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ)
?.active ?? false;
return {
isRolesEnabled: isValidLicense && isFineGrainedAuthzEnabled,
isLoading:
(isFetchingActiveLicense && !activeLicense) ||
(isFetchingFeatureFlags && !featureFlags),
isRolesEnabled: isValidLicense,
isLoading: isFetchingActiveLicense && !activeLicense,
};
};

View File

@@ -7,7 +7,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import getMinAgo from './getStartAndEndTime/getMinAgo';
const validCustomTimeRegex = /^(\d+)([mhdw])$/;
const validCustomTimeRegex = /^(\d+)(months?|[mhdw])$/;
export const isValidShortHandDateTimeFormat = (time: string): boolean =>
validCustomTimeRegex.test(time);

View File

@@ -52,12 +52,12 @@ const makeDashboard = (
...overrides,
}) as unknown as DashboardListItem;
const renderRow = (dashboard: DashboardListItem): void => {
const renderRow = (dashboard: DashboardListItem, canEdit = true): void => {
render(
<DashboardRow
dashboard={dashboard}
index={0}
canEdit
canEdit={canEdit}
showUpdatedAt={false}
showUpdatedBy={false}
/>,
@@ -105,6 +105,19 @@ 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();
});
});
});

View File

@@ -252,6 +252,7 @@ function DashboardRow({
open={isLegacyDialogOpen}
dashboardId={id}
dashboardName={name}
canEdit={canEdit}
onClose={(): void => setIsLegacyDialogOpen(false)}
/>
)}

View File

@@ -49,6 +49,7 @@
.footer {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}

View File

@@ -18,19 +18,30 @@ 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): void => {
const setup = ({ open = true, canEdit = true } = {}): void => {
render(
<LegacyDashboardDialog
open={open}
dashboardId={DASHBOARD_ID}
dashboardName="My Legacy Dashboard"
canEdit={canEdit}
onClose={jest.fn()}
/>,
);
@@ -57,8 +68,31 @@ 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(false);
setup({ open: false });
expect(screen.queryByTestId('legacy-dashboard-id')).not.toBeInTheDocument();
});
});

View File

@@ -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 } from '@signozhq/icons';
import { ArrowUpRight, Copy, RotateCw } from '@signozhq/icons';
import { useCopyToClipboard } from 'react-use';
import { toast } from '@signozhq/ui/sonner';
import logEvent from 'api/common/logEvent';
@@ -9,28 +9,34 @@ 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 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.
* 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.
*/
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);
@@ -49,6 +55,14 @@ 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"
@@ -65,14 +79,15 @@ function LegacyDashboardDialog({
variant="ghost"
color="secondary"
size="md"
disabled={isMigrating}
onClick={onClose}
testId="legacy-dashboard-close"
>
Close
</Button>
<Button
variant="solid"
color="primary"
variant={canEdit ? 'outlined' : 'solid'}
color={canEdit ? 'secondary' : 'primary'}
size="md"
suffix={<ArrowUpRight size={14} />}
onClick={onContactSupport}
@@ -80,6 +95,20 @@ 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>
}
>
@@ -87,8 +116,10 @@ function LegacyDashboardDialog({
<Typography.Text className={styles.description}>
<strong>{dashboardName || 'This dashboard'}</strong> hasn&apos;t been
migrated to the new dashboard experience yet, so it can&apos;t be opened
here. Share the dashboard ID below with support and we&apos;ll help you
move it over.
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."}
</Typography.Text>
<div className={styles.idField}>

View File

@@ -0,0 +1,109 @@
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);
});
});

View File

@@ -0,0 +1,48 @@
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 };
}

View File

@@ -114,6 +114,23 @@ 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
@@ -142,6 +159,13 @@ 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`

View File

@@ -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: term.start + slot.replaceStartRel,
replaceStart: Math.min(term.start + slot.replaceStartRel, pos),
replaceEnd: pos,
};
};

View File

@@ -1,47 +1,15 @@
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 InfraMonitoringHosts from 'container/InfraMonitoringHosts';
import InfraMonitoringK8s from 'container/InfraMonitoringK8s';
import { useIsInfraMonitoringV2 } from 'hooks/useIsInfraMonitoringV2';
import InfraMonitoringHostsV2 from 'container/InfraMonitoringHostsV2';
import InfraMonitoringK8sV2 from 'container/InfraMonitoringK8sV2';
import { Inbox } from '@signozhq/icons';
const InfraMonitoringHostsV2 = Loadable(
() => import('container/InfraMonitoringHostsV2'),
);
const InfraMonitoringK8sV2 = Loadable(
() => import('container/InfraMonitoringK8sV2'),
);
function HostsContainer(): JSX.Element {
const isInfraMonitoringV2 = useIsInfraMonitoringV2();
if (isInfraMonitoringV2) {
return (
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<InfraMonitoringHostsV2 />
</Suspense>
);
}
return <InfraMonitoringHosts />;
return <InfraMonitoringHostsV2 />;
}
function KubernetesContainer(): JSX.Element {
const isInfraMonitoringV2 = useIsInfraMonitoringV2();
if (isInfraMonitoringV2) {
return (
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<InfraMonitoringK8sV2 />
</Suspense>
);
}
return <InfraMonitoringK8s />;
return <InfraMonitoringK8sV2 />;
}
export const Hosts: TabRoutes = {

View File

@@ -150,13 +150,6 @@ export const defaultFeatureFlags = [
usage_limit: -1,
route: '',
},
{
name: FeatureKeys.USE_FINE_GRAINED_AUTHZ,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
];
export function getAppContextMock(

View File

@@ -69,7 +69,12 @@ export const getMetricsExplorerUrl = ({
const params = new URLSearchParams();
params.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(query)),
// `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 })),
);
if (relativeTime) {

View File

@@ -112,15 +112,12 @@ functionCall
;
/*
* 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.
* 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.
*/
searchCall
: SEARCH LPAREN functionParamList RPAREN
: SEARCH LPAREN valueList RPAREN
;
// Function parameters can be keys, single scalar values, or arrays

View File

@@ -241,9 +241,12 @@ func (server *Server) PutAlerts(ctx context.Context, postableAlerts alertmanager
}
func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertmanagertypes.Config) error {
config := alertmanagerConfig.AlertmanagerConfig()
resolved, err := alertmanagerConfig.Resolved()
if err != nil {
return err
}
config := resolved.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
@@ -275,7 +278,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 := alertmanagerConfig.GetReceiver(rcv.Name)
extendedRcv, err := resolved.GetReceiver(rcv.Name)
if err != nil {
return err
}
@@ -350,7 +353,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
go server.dispatcher.Run()
go server.inhibitor.Run()
server.alertmanagerConfig = alertmanagerConfig
server.alertmanagerConfig = resolved
return nil
}

View File

@@ -1,7 +1,6 @@
package alertmanager
import (
"net/url"
"strings"
"time"
@@ -26,11 +25,6 @@ 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)
}

View File

@@ -167,6 +167,10 @@ 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
}
@@ -217,6 +221,10 @@ 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
}

View File

@@ -85,6 +85,23 @@ 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"},

View File

@@ -29,13 +29,18 @@ 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 enabled {
cfg.Config.Boolean = map[string]bool{
flagger.FeatureUseJSONBody.String(): true,
}
if len(flags) > 0 {
cfg.Config.Boolean = flags
}
fl, err := flagger.New(
context.Background(),

View File

@@ -10,11 +10,8 @@ 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")
)
@@ -76,14 +73,6 @@ 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,
@@ -100,14 +89,6 @@ 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,

View File

@@ -0,0 +1,40 @@
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)
}
})
}
}

View File

@@ -621,7 +621,7 @@
{
"metricName": "cloudsql.googleapis.com/database/postgresql/deadlock_count",
"temporality": "",
"timeAggregation": "max",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "avg"
}
@@ -882,7 +882,7 @@
{
"metricName": "cloudsql.googleapis.com/database/postgresql/transaction_count",
"temporality": "",
"timeAggregation": "max",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "avg"
}

View File

@@ -54,13 +54,13 @@
{
"name": "cloudsql.googleapis.com/database/postgresql/transaction_count",
"unit": "Count",
"type": "Gauge",
"type": "Sum",
"description": ""
},
{
"name": "cloudsql.googleapis.com/database/postgresql/deadlock_count",
"unit": "Count",
"type": "Gauge",
"type": "Sum",
"description": ""
},
{

View File

@@ -925,7 +925,7 @@
"metricName": "compute.googleapis.com/instance/disk/average_io_latency",
"temporality": "",
"timeAggregation": "avg",
"spaceAggregation": "sum",
"spaceAggregation": "max",
"reduceTo": "avg"
}
],

View File

@@ -1207,8 +1207,8 @@
{
"metricName": "kubernetes.io/container/restart_count",
"temporality": "",
"timeAggregation": "max",
"spaceAggregation": "max",
"timeAggregation": "increase",
"spaceAggregation": "sum",
"reduceTo": "avg"
}
],

View File

@@ -601,8 +601,8 @@
{
"metricName": "redis.googleapis.com/stats/cache_hit_ratio",
"temporality": "",
"timeAggregation": "max",
"spaceAggregation": "sum",
"timeAggregation": "min",
"spaceAggregation": "min",
"reduceTo": "avg"
}
],
@@ -788,7 +788,7 @@
"metricName": "redis.googleapis.com/commands/usec_per_call",
"temporality": "",
"timeAggregation": "max",
"spaceAggregation": "sum",
"spaceAggregation": "max",
"reduceTo": "avg"
}
],
@@ -945,7 +945,7 @@
{
"metricName": "redis.googleapis.com/commands/calls",
"temporality": "",
"timeAggregation": "max",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "avg"
}
@@ -1044,8 +1044,8 @@
{
"metricName": "redis.googleapis.com/stats/reject_connections_count",
"temporality": "",
"timeAggregation": "avg",
"spaceAggregation": "avg",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "avg"
}
],
@@ -1213,4 +1213,4 @@
"refreshInterval": "",
"links": []
}
}
}

View File

@@ -63,6 +63,9 @@ 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)
@@ -132,6 +135,8 @@ type Handler interface {
GetV2(http.ResponseWriter, *http.Request)
MigrateV2(http.ResponseWriter, *http.Request)
ListV2(http.ResponseWriter, *http.Request)
ListForUserV2(http.ResponseWriter, *http.Request)

View File

@@ -207,6 +207,38 @@ 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)
}

View File

@@ -4,6 +4,7 @@ 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"
@@ -19,7 +20,6 @@ func (m *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy stri
}
dashboard := postable.NewDashboardV2(orgID, createdBy, source)
var storableDashboard *dashboardtypes.StorableDashboard
err := m.store.RunInTx(ctx, func(ctx context.Context) error {
resolvedTags, err := m.tagModule.SyncTags(ctx, orgID, coretypes.KindDashboard, dashboard.ID, postable.Tags)
@@ -32,14 +32,13 @@ func (m *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy stri
if err != nil {
return err
}
storableDashboard = storable
return m.store.Create(ctx, storable)
})
if err != nil {
return nil, err
}
m.analytics.TrackUser(ctx, orgID.String(), creator.String(), "Dashboard Created", dashboardtypes.NewStatsFromStorableDashboards([]*dashboardtypes.StorableDashboard{storableDashboard}))
m.analytics.TrackUser(ctx, orgID.String(), creator.String(), "Dashboard Created", dashboardtypes.NewStatsFromPostableDashboardV2(postable))
return dashboard, nil
}
@@ -121,6 +120,51 @@ 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

View File

@@ -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", llmpricingruletypes.GenAIRequestModel)},
Filter: &qbtypes.Filter{Expression: fmt.Sprintf("%s EXISTS", telemetrytypes.GenAIRequestModel)},
Aggregations: []qbtypes.TraceAggregation{
{Expression: "count()", Alias: "spanCount"},
},
GroupBy: []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: llmpricingruletypes.GenAIRequestModel,
Name: telemetrytypes.GenAIRequestModel,
FieldContext: telemetrytypes.FieldContextSpan,
FieldDataType: telemetrytypes.FieldDataTypeString,
}},
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: llmpricingruletypes.GenAIProviderName,
Name: telemetrytypes.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 llmpricingruletypes.GenAIRequestModel:
case telemetrytypes.GenAIRequestModel:
modelIdx = i
case llmpricingruletypes.GenAIProviderName:
case telemetrytypes.GenAIProviderName:
providerIdx = i
}
case qbtypes.ColumnTypeAggregation:

View File

@@ -35,7 +35,7 @@ func (c *conditionBuilder) ConditionFor(
sb *sqlbuilder.SelectBuilder,
) ([]string, []string, error) {
// has/hasAny/hasAll/hasToken are logs-body-only; reject for rule state history.
// has/hasAny/hasAll/hasToken/search are logs-only functions; 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

View File

@@ -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, 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,
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,
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
FunctionParamList() IFunctionParamListContext
ValueList() IValueListContext
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) FunctionParamList() IFunctionParamListContext {
func (s *SearchCallContext) ValueList() IValueListContext {
var t antlr.RuleContext
for _, ctx := range s.GetChildren() {
if _, ok := ctx.(IFunctionParamListContext); ok {
if _, ok := ctx.(IValueListContext); ok {
t = ctx.(antlr.RuleContext)
break
}
@@ -3005,7 +3005,7 @@ func (s *SearchCallContext) FunctionParamList() IFunctionParamListContext {
return nil
}
return t.(IFunctionParamListContext)
return t.(IValueListContext)
}
func (s *SearchCallContext) RPAREN() antlr.TerminalNode {
@@ -3064,7 +3064,7 @@ func (p *FilterQueryParser) SearchCall() (localctx ISearchCallContext) {
}
{
p.SetState(202)
p.FunctionParamList()
p.ValueList()
}
{
p.SetState(203)

View File

@@ -250,7 +250,7 @@ func (handler *handler) ReplaceVariables(rw http.ResponseWriter, req *http.Reque
errs := []error{}
for idx, item := range queryRangeRequest.CompositeQuery.Queries {
if item.Type == qbtypes.QueryTypeBuilder {
if item.Type == qbtypes.QueryTypeBuilder || item.Type == qbtypes.QueryTypeBuilderAI {
switch spec := item.Spec.(type) {
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
if spec.Filter != nil && spec.Filter.Expression != "" {

View File

@@ -20,6 +20,8 @@ import (
"github.com/SigNoz/signoz/pkg/valuer"
)
const estimateTimeout = 5 * time.Second
const traceOutsideRangeWarn = "Query %s references a trace_id that exists between %s and %s (UTC) but lies outside the selected time range; adjust the time range to see results"
type builderQuery[T any] struct {
@@ -247,6 +249,10 @@ func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error)
return nil, err
}
if err := q.enforceEstimate(ctx, stmt); err != nil {
return nil, err
}
// Execute the query with proper context for partial value detection
result, err := q.executeWithContext(ctx, stmt.Query, stmt.Args)
if err != nil {
@@ -258,6 +264,65 @@ func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error)
return result, nil
}
// estimateRows returns the per-shard EXPLAIN ESTIMATE scan rows for a cost-guarded
// statement. guarded=false means nothing to enforce; a non-nil error means reject.
// Callers own the budget comparison (per-statement or cumulative).
func (q *builderQuery[T]) estimateRows(ctx context.Context, stmt *qbtypes.Statement) (int64, bool, error) {
if stmt.CostGuard == nil || stmt.CostGuard.MaxScanRows <= 0 {
return 0, false, nil
}
estCtx, cancel := context.WithTimeout(ctx, estimateTimeout)
defer cancel()
entries, err := q.telemetryStore.Estimate(estCtx, stmt.Query, stmt.Args...)
if err != nil {
if ctx.Err() != nil {
return 0, true, ctx.Err()
}
if errors.Is(estCtx.Err(), context.DeadlineExceeded) {
return 0, true, errors.NewInvalidInputf(errors.CodeInvalidInput,
"This query is too broad to plan within %s", estimateTimeout).
WithSuggestions(costGuardSuggestions(stmt.CostGuard.Warning)...)
}
return 0, true, err
}
var rows int64
for _, e := range entries {
rows += e.Rows
}
return rows, true, nil
}
// enforceEstimate rejects a scan-heavy statement whose estimate exceeds its own
// budget, before executing. Budget 0 disables.
func (q *builderQuery[T]) enforceEstimate(ctx context.Context, stmt *qbtypes.Statement) error {
rows, guarded, err := q.estimateRows(ctx, stmt)
if err != nil {
return err
}
if !guarded {
return nil
}
if budget := stmt.CostGuard.MaxScanRows; rows > budget {
return errors.NewInvalidInputf(errors.CodeInvalidInput,
"This query would scan about %d rows per shard in this range, over the per-shard limit of %d", rows, budget).
WithSuggestions(costGuardSuggestions(stmt.CostGuard.Warning)...)
}
return nil
}
// costGuardSuggestions leads with the requirement's advisory (e.g. the search() hint),
// then how to get under budget.
func costGuardSuggestions(advisory string) []string {
suggestions := make([]string, 0, 2)
if advisory != "" {
suggestions = append(suggestions, advisory)
}
return append(suggestions, "Narrow the time range or add a more selective filter.")
}
// narrowWindowByTraceID inspects the filter for trace_id predicates and clamps
// [fromMS,toMS] to the time range stored in signoz_traces.distributed_trace_summary.
// Returns the (possibly narrowed) window, overlap=false when the trace lies
@@ -491,6 +556,10 @@ func (q *builderQuery[T]) executeWindowList(ctx context.Context) (*qbtypes.Resul
var warnings []string
var warningsDocURL string
// Cumulative across visited buckets: the budget bounds the whole query's per-shard
// scan, not each bucket independently.
var estimatedScan int64
for _, r := range buckets {
q.spec.Offset = 0
q.spec.Limit = need
@@ -501,6 +570,18 @@ func (q *builderQuery[T]) executeWindowList(ctx context.Context) (*qbtypes.Resul
}
warnings = stmt.Warnings
warningsDocURL = stmt.WarningsDocURL
rowsEst, guarded, err := q.estimateRows(ctx, stmt)
if err != nil {
return nil, err
}
if guarded {
estimatedScan += rowsEst
if budget := stmt.CostGuard.MaxScanRows; estimatedScan > budget {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput,
"This query would scan about %d rows per shard across the time range, over the per-shard limit of %d", estimatedScan, budget).
WithSuggestions(costGuardSuggestions(stmt.CostGuard.Warning)...)
}
}
// Execute with proper context for partial value detection
res, err := q.executeWithContext(ctx, stmt.Query, stmt.Args)
if err != nil {

View File

@@ -249,6 +249,7 @@ func (q *querier) buildPreviewProviders(
func rendersStandaloneStatement(t qbtypes.QueryType) bool {
switch t {
case qbtypes.QueryTypeBuilder,
qbtypes.QueryTypeBuilderAI,
qbtypes.QueryTypePromQL,
qbtypes.QueryTypeClickHouseSQL,
qbtypes.QueryTypeTraceOperator:

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