Compare commits

..

3 Commits

Author SHA1 Message Date
Abhi Kumar
8f06859ebf fix(dashboards-v2): refine New Panel modal footer + section labels
- Keep the footer visible at all times; the "Add Panel" confirm is disabled
  until a panel type is selected (instead of hiding the whole footer).
- Footer layout: the section selector fills the available width and the
  confirm button takes its natural width (no more 50/50 split).
- Add a "Select panel type" label above the panel-type grid, matching the
  existing "Add panel to" label.
2026-07-06 19:35:56 +05:30
Abhi Kumar
7ea3ba4e09 feat(dashboards-v2): choose target section when adding a panel
Add a section picker to the New Panel modal so a panel can be placed in
any section (or the untitled root), not just the one the "Add panel"
action was triggered from. Sections are read from the cached dashboard
query via useDashboardSections (no refetch, no prop-drilling).

Picking a panel type now selects it (highlighted card) and reveals a
footer holding the section dropdown and an "Add Panel" button split
50/50; confirming shows a brief loader before navigating to the editor.
The chosen section's layoutIndex is threaded through
useCreatePanel.createPanel.
2026-07-06 19:35:56 +05:30
Abhi Kumar
af02e8b722 fix: ui fixes for panel selection modal 2026-07-06 19:35:56 +05:30
100 changed files with 1356 additions and 4507 deletions

View File

@@ -4261,8 +4261,6 @@ components:
$ref: '#/components/schemas/InframonitoringtypesNodeCountsByReadiness'
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
required:
- clusterName
- clusterCPU
@@ -4271,7 +4269,6 @@ components:
- clusterMemoryAllocatable
- nodeCountsByReadiness
- podCountsByPhase
- podCountsByStatus
- meta
type: object
InframonitoringtypesClusters:
@@ -4327,8 +4324,6 @@ components:
type: object
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
required:
- daemonSetName
- daemonSetCPU
@@ -4340,7 +4335,6 @@ components:
- desiredNodes
- currentNodes
- podCountsByPhase
- podCountsByStatus
- meta
type: object
InframonitoringtypesDaemonSets:
@@ -4396,8 +4390,6 @@ components:
type: object
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
required:
- deploymentName
- deploymentCPU
@@ -4409,7 +4401,6 @@ components:
- desiredPods
- availablePods
- podCountsByPhase
- podCountsByStatus
- meta
type: object
InframonitoringtypesDeployments:
@@ -4542,8 +4533,6 @@ components:
type: object
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
successfulPods:
type: integer
required:
@@ -4559,7 +4548,6 @@ components:
- failedPods
- successfulPods
- podCountsByPhase
- podCountsByStatus
- meta
type: object
InframonitoringtypesJobs:
@@ -4650,14 +4638,11 @@ components:
type: string
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
required:
- namespaceName
- namespaceCPU
- namespaceMemory
- podCountsByPhase
- podCountsByStatus
- meta
type: object
InframonitoringtypesNamespaces:
@@ -4723,14 +4708,11 @@ components:
type: string
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
required:
- nodeName
- condition
- nodeCountsByReadiness
- podCountsByPhase
- podCountsByStatus
- nodeCPU
- nodeCPUAllocatable
- nodeMemory
@@ -4776,64 +4758,6 @@ components:
- failed
- unknown
type: object
InframonitoringtypesPodCountsByStatus:
properties:
completed:
type: integer
containerCannotRun:
type: integer
containerCreating:
type: integer
crashLoopBackOff:
type: integer
createContainerConfigError:
type: integer
errImagePull:
type: integer
error:
type: integer
evicted:
type: integer
failed:
type: integer
imagePullBackOff:
type: integer
nodeAffinity:
type: integer
nodeLost:
type: integer
oomKilled:
type: integer
pending:
type: integer
running:
type: integer
shutdown:
type: integer
unexpectedAdmissionError:
type: integer
unknown:
type: integer
required:
- pending
- running
- failed
- unknown
- crashLoopBackOff
- imagePullBackOff
- errImagePull
- createContainerConfigError
- containerCreating
- oomKilled
- completed
- error
- containerCannotRun
- evicted
- nodeAffinity
- nodeLost
- shutdown
- unexpectedAdmissionError
type: object
InframonitoringtypesPodPhase:
enum:
- pending
@@ -4864,8 +4788,6 @@ components:
type: number
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
podMemory:
format: double
type: number
@@ -4877,11 +4799,6 @@ components:
type: number
podPhase:
$ref: '#/components/schemas/InframonitoringtypesPodPhase'
podRestarts:
format: int64
type: integer
podStatus:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
podUID:
type: string
required:
@@ -4894,34 +4811,9 @@ components:
- podMemoryLimit
- podPhase
- podCountsByPhase
- podStatus
- podCountsByStatus
- podRestarts
- podAge
- meta
type: object
InframonitoringtypesPodStatus:
enum:
- pending
- running
- failed
- unknown
- crashloopbackoff
- imagepullbackoff
- errimagepull
- createcontainerconfigerror
- containercreating
- oomkilled
- completed
- error
- containercannotrun
- evicted
- nodeaffinity
- nodelost
- shutdown
- unexpectedadmissionerror
- no_data
type: string
InframonitoringtypesPods:
properties:
endTimeBeforeRetention:
@@ -5220,8 +5112,6 @@ components:
type: object
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
statefulSetCPU:
format: double
type: number
@@ -5253,7 +5143,6 @@ components:
- desiredPods
- currentPods
- podCountsByPhase
- podCountsByStatus
- meta
type: object
InframonitoringtypesStatefulSets:

View File

@@ -1,149 +0,0 @@
# Authz
SigNoz uses [OpenFGA](https://openfga.dev/), a relationship-based access control (ReBAC) system, to authorize every request. Transactions are never attached to users directly — they are attached to **roles** (as relationship tuples in OpenFGA), and users or service accounts (principals) are made **assignees** of those roles. The central interface is `AuthZ` in [pkg/authz/authz.go](/pkg/authz/authz.go), backed by an embedded OpenFGA server in [pkg/authz/openfgaserver](/pkg/authz/openfgaserver/server.go).
As a feature author, you will rarely touch OpenFGA directly. You interact with two layers:
1. **The registries** in [pkg/types/coretypes](/pkg/types/coretypes) — where every resource, verb, and managed-role permission is declared in code.
2. **The route wiring** in [pkg/apiserver/signozapiserver](/pkg/apiserver/signozapiserver) — where each route declares what resource it touches and which verb it needs, and the middleware turns that into a check.
## What are the building blocks?
### Type
A `Type` is the coarse FGA type of a resource. The set is finite and defined in [pkg/types/coretypes/registry_type.go](/pkg/types/coretypes/registry_type.go): `user`, `serviceaccount`, `anonymous`, `role`, `organization`, `metaresource`, and `telemetryresource`. Each type carries a selector regex (what a valid ID looks like) and the verbs allowed on it.
Almost every feature resource is a `metaresource` (dashboards, rules, pipelines, ...) or a `telemetryresource` (logs, traces, metrics). You should almost never need a new type.
### Verb
A `Verb` is the action being authorized. All verbs are defined in [pkg/types/coretypes/registry_verb.go](/pkg/types/coretypes/registry_verb.go): `create`, `read`, `update`, `delete`, `list`, `assignee`, `attach`, and `detach`.
- `assignee` is special: it is the membership relation between a subject and a role ("user X is an assignee of role Y"), not an action a route checks for.
- `attach`/`detach` authorize linking two resources together (e.g. assigning a role to a service account).
### Kind
A `Kind` is the fine-grained name of your resource — `dashboard`, `rule`, `quick-filter`. Kinds are registered in [pkg/types/coretypes/registry_kind.go](/pkg/types/coretypes/registry_kind.go). A kind rides on top of an existing type, so adding one does **not** require any OpenFGA schema change.
### Resource
A `Resource` ties a type and a kind together and knows how to render itself as an FGA object string. The interface lives in [pkg/types/coretypes/resource.go](/pkg/types/coretypes/resource.go):
```go
type Resource interface {
Type() Type
Kind() Kind
Prefix(orgId valuer.UUID) string // metaresource:organization/<orgID>/dashboard
Object(orgId valuer.UUID, selector string) string
Scope(verb Verb) string // dashboard:read
AllowedVerbs() []Verb
}
```
All resources are registered in [pkg/types/coretypes/registry_resource.go](/pkg/types/coretypes/registry_resource.go) using constructors like `NewResourceMetaResource(KindDashboard)` or `NewResourceTelemetryResource(KindLogs)`.
### Selector
A `Selector` identifies *which* instance(s) of a resource a check is about — a UUID, a role name, or the wildcard `*`. A `SelectorFunc` maps the extracted resource ID to selectors at request time. Two prebuilt ones cover most routes ([pkg/types/coretypes/selector.go](/pkg/types/coretypes/selector.go)):
- `WildcardSelector` — the check is against all instances of the resource (`create`, `list`).
- `IDSelector` — the check is against the specific instance *or* the wildcard (`read`, `update`, `delete` of one object). A subject authorized on `*` is authorized on every instance.
When the ID in the request is not what FGA needs (e.g. routes receive a role UUID but FGA objects use role names), write a custom `SelectorFunc` — see `roleSelector` in [pkg/apiserver/signozapiserver/serviceaccount.go](/pkg/apiserver/signozapiserver/serviceaccount.go).
### Roles, transactions, and tuples
SigNoz ships four managed roles, declared in [pkg/types/coretypes/registry_managed_role.go](/pkg/types/coretypes/registry_managed_role.go): `signoz-admin`, `signoz-editor`, `signoz-viewer`, and `signoz-anonymous`. Their permissions are declared in code as `Transaction`s (a verb on an object) in `ManagedRoleToTransactions` — this map is the single source of truth for what each managed role can do.
At organization bootstrap, `CreateManagedRoles` and `CreateManagedUserRoleTransactions` (see [pkg/authz/authz.go](/pkg/authz/authz.go)) persist the role rows and write one OpenFGA tuple per transaction, linking `role:organization/<orgID>/role/<name>#assignee` to each permitted object. Users and service accounts are then granted roles via `Grant`/`Revoke`, which writes `assignee` tuples. Custom roles (enterprise) are managed through the roles API in [pkg/authz/signozauthzapi/handler.go](/pkg/authz/signozauthzapi/handler.go).
### Schema
The OpenFGA authorization model is a hand-written DSL, embedded at build time: [pkg/authz/openfgaschema/base.fga](/pkg/authz/openfgaschema/base.fga) for community and [ee/authz/openfgaschema/base.fga](/ee/authz/openfgaschema/base.fga) for enterprise. The community model only supports role assignment; the enterprise model defines per-verb relations on every type, enabling genuine per-resource checks. This split is why `CheckWithTupleCreation` behaves differently per edition (see [How does a check work at runtime?](#how-does-a-check-work-at-runtime)). You only touch these files when introducing a brand-new **type** — never for a new kind.
## How do I add authz to my feature?
### 1. Register the kind
Add your kind in [pkg/types/coretypes/registry_kind.go](/pkg/types/coretypes/registry_kind.go) and append it to `Kinds`:
```go
KindThing = MustNewKind("thing")
```
### 2. Register the resource
Add the resource in [pkg/types/coretypes/registry_resource.go](/pkg/types/coretypes/registry_resource.go) and append it to `Resources`:
```go
ResourceMetaResourceThing = NewResourceMetaResource(KindThing)
```
Pass an explicit verb list to `NewResourceMetaResource` only if your resource supports fewer verbs than the type default.
### 3. Grant permissions to managed roles
Decide what each managed role can do with your resource and add the transactions in [pkg/types/coretypes/registry_managed_role.go](/pkg/types/coretypes/registry_managed_role.go):
```go
// thing — editors manage, viewers read
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindThing}, WildCardSelectorString)},
```
The convention so far: admin gets everything, editor gets CRUD on day-to-day observability resources, viewer gets `read`/`list`, anonymous gets nothing (except public dashboards).
### 4. Wire the route
Register the route in [pkg/apiserver/signozapiserver](/pkg/apiserver/signozapiserver), wrapping your handler with `CheckResources` and declaring what the route touches via a `ResourceDef` ([pkg/http/handler/resourcedef.go](/pkg/http/handler/resourcedef.go)). A complete example from [pkg/apiserver/signozapiserver/serviceaccount.go](/pkg/apiserver/signozapiserver/serviceaccount.go):
```go
router.Handle("/api/v1/service_accounts", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.Create, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "CreateServiceAccount",
// ...
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbCreate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbCreate,
Category: coretypes.ActionCategoryAccessControl,
ID: coretypes.ResponseJSONPath("data.id"),
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodPost)
```
The pieces:
- **`CheckResources(handlerFn, roles...)`** — the resource-aware authorization wrapper from [pkg/http/middleware/authz.go](/pkg/http/middleware/authz.go). The role list is the community-edition fallback: which managed roles may call this route when per-resource checks are unavailable.
- **`ResourceDef`** — declares the resource, verb, audit category, how to extract the instance ID, and how to turn that ID into selectors. ID extractors live in [pkg/types/coretypes/extractor.go](/pkg/types/coretypes/extractor.go): `PathParam("id")`, `BodyJSONPath("data.id")`, `BodyJSONArray("ids")`, and `ResponseJSONPath("data.id")` for IDs only known after the handler runs (e.g. `create`).
- **`SecuritySchemes`** — advertises the required scope (`resource.Scope(verb)`, e.g. `serviceaccount:create`) in the OpenAPI spec.
For routes that link two resources, use `AttachDetachSiblingResourceDef` (both sides are authz-checked, e.g. attaching a role to a service account requires `attach` on **both** the service account and the role). For parent-child routes (e.g. creating an API key under a service account), both sides are checked too, but with different verbs: declare a `BasicResourceDef` checking the child with `create`/`delete`, alongside an `AttachDetachParentChildResourceDef` checking the parent with `attach`/`detach` (within that def the child is only recorded for audit) — see the `/api/v1/service_accounts/{id}/keys` route in [pkg/apiserver/signozapiserver/serviceaccount.go](/pkg/apiserver/signozapiserver/serviceaccount.go).
Prefer `CheckResources` with a `ResourceDef` for anything resource-shaped. The older coarse gates `ViewAccess`/`EditAccess`/`AdminAccess` only check "does the caller hold one of these roles" and give up per-resource granularity; `OpenAccess` performs no authorization (authentication still applies); `CheckWithoutClaims` serves anonymous routes such as public dashboards.
### 5. Backfill existing organizations (only if needed)
Managed-role tuples are written from the registry at organization creation, so **new resources need no migration for new organizations**. If existing organizations must get the new permissions, add a migration in [pkg/sqlmigration](/pkg/sqlmigration) that inserts the tuples — see [083_add_role_crud_tuples.go](/pkg/sqlmigration/083_add_role_crud_tuples.go) for the pattern.
## How does a check work at runtime?
1. The resource middleware ([pkg/http/middleware/resource.go](/pkg/http/middleware/resource.go)) runs on every request. It reads the matched handler's `ResourceDef`s, extracts the resource IDs from path/body, and stores the resolved resources in the request context.
2. `CheckResources` reads them back, runs each `SelectorFunc`, and calls `AuthZ.CheckWithTupleCreation(ctx, claims, orgID, relation, resource, selectors, roleSelectors)`.
3. What happens next depends on the edition:
- **Community** ([pkg/authz/openfgaserver/server.go](/pkg/authz/openfgaserver/server.go)) ignores the resource and selectors and only checks whether the subject is an `assignee` of one of the allowed roles — a plain role gate.
- **Enterprise** ([ee/authz/openfgaserver/server.go](/ee/authz/openfgaserver/server.go)) builds tuples via `authtypes.NewTuples` — subject `user:organization/<orgID>/user/<userID>`, relation `create`, object `serviceaccount:organization/<orgID>/serviceaccount/*` — and batch-checks them against OpenFGA: genuine per-resource authorization, including custom roles.
Because both paths go through the same middleware and the same `ResourceDef` declarations, a route wired once works correctly in both editions.
## What should I remember?
- Declare authz in the registries ([pkg/types/coretypes](/pkg/types/coretypes)), not in migrations — tuples for new organizations are derived from code at bootstrap.
- A new kind never needs an OpenFGA schema change; only a new type does, and then **both** [pkg/authz/openfgaschema/base.fga](/pkg/authz/openfgaschema/base.fga) and [ee/authz/openfgaschema/base.fga](/ee/authz/openfgaschema/base.fga) must be updated together.
- Prefer `CheckResources` + `ResourceDef` over the coarse `ViewAccess`/`EditAccess`/`AdminAccess` gates for new routes.
- Use `WildcardSelector` for `create`/`list`, `IDSelector` for instance operations, and a custom `SelectorFunc` when the request ID is not the FGA selector.
- Linking routes check **both** sides: peers via `AttachDetachSiblingResourceDef` (same verb on both), parent-child via a `BasicResourceDef` on the child (`create`/`delete`) paired with an `AttachDetachParentChildResourceDef` on the parent (`attach`/`detach`).
- Changing `ManagedRoleToTransactions` only affects organizations created afterwards — add a [pkg/sqlmigration](/pkg/sqlmigration) migration to backfill existing ones.

View File

@@ -11,7 +11,6 @@ We adhere to three primary style guides as our foundation:
We **recommend** (almost enforce) reviewing these guides before contributing to the codebase. They provide valuable insights into writing idiomatic Go code and will help you understand our approach to backend development. In addition, we have a few additional rules that make certain areas stricter than the above which can be found in area-specific files in this package:
- [Abstractions](abstractions.md) - When to introduce new types and intermediate representations
- [Authz](authz.md) - Authorization, roles, and access control
- [Errors](errors.md) - Structured error handling
- [Endpoint](endpoint.md) - HTTP endpoint patterns
- [Flagger](flagger.md) - Feature flag patterns

View File

@@ -25,7 +25,6 @@ You are operating within a constrained context window and strict system prompts.
- Never create barrel files.
- When writing new css, prefer CSS Modules
- Use ./docs/css-modules-guide.md as reference on how to write good CSS Modules.
- When writing code that could need authorization checks, read ./src/lib/authz/README.md
3. FORCED VERIFICATION: Your internal tools mark file writes as successful even if the code does not compile. You are FORBIDDEN from reporting a task as complete until you have:
- Run `pnpm tsgo --noEmit`

View File

@@ -5704,81 +5704,6 @@ export interface InframonitoringtypesPodCountsByPhaseDTO {
unknown: number;
}
export interface InframonitoringtypesPodCountsByStatusDTO {
/**
* @type integer
*/
completed: number;
/**
* @type integer
*/
containerCannotRun: number;
/**
* @type integer
*/
containerCreating: number;
/**
* @type integer
*/
crashLoopBackOff: number;
/**
* @type integer
*/
createContainerConfigError: number;
/**
* @type integer
*/
errImagePull: number;
/**
* @type integer
*/
error: number;
/**
* @type integer
*/
evicted: number;
/**
* @type integer
*/
failed: number;
/**
* @type integer
*/
imagePullBackOff: number;
/**
* @type integer
*/
nodeAffinity: number;
/**
* @type integer
*/
nodeLost: number;
/**
* @type integer
*/
oomKilled: number;
/**
* @type integer
*/
pending: number;
/**
* @type integer
*/
running: number;
/**
* @type integer
*/
shutdown: number;
/**
* @type integer
*/
unexpectedAdmissionError: number;
/**
* @type integer
*/
unknown: number;
}
export interface InframonitoringtypesClusterRecordDTO {
/**
* @type number
@@ -5810,7 +5735,6 @@ export interface InframonitoringtypesClusterRecordDTO {
meta: InframonitoringtypesClusterRecordDTOMeta;
nodeCountsByReadiness: InframonitoringtypesNodeCountsByReadinessDTO;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
}
export enum InframonitoringtypesResponseTypeDTO {
@@ -5914,7 +5838,6 @@ export interface InframonitoringtypesDaemonSetRecordDTO {
*/
meta: InframonitoringtypesDaemonSetRecordDTOMeta;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
}
export interface InframonitoringtypesDaemonSetsDTO {
@@ -5992,7 +5915,6 @@ export interface InframonitoringtypesDeploymentRecordDTO {
*/
meta: InframonitoringtypesDeploymentRecordDTOMeta;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
}
export interface InframonitoringtypesDeploymentsDTO {
@@ -6159,7 +6081,6 @@ export interface InframonitoringtypesJobRecordDTO {
*/
meta: InframonitoringtypesJobRecordDTOMeta;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
/**
* @type integer
*/
@@ -6213,7 +6134,6 @@ export interface InframonitoringtypesNamespaceRecordDTO {
*/
namespaceName: string;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
}
export interface InframonitoringtypesNamespacesDTO {
@@ -6280,7 +6200,6 @@ export interface InframonitoringtypesNodeRecordDTO {
*/
nodeName: string;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
}
export interface InframonitoringtypesNodesDTO {
@@ -6318,27 +6237,6 @@ export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
export type InframonitoringtypesPodRecordDTOMeta =
InframonitoringtypesPodRecordDTOMetaAnyOf | null;
export enum InframonitoringtypesPodStatusDTO {
pending = 'pending',
running = 'running',
failed = 'failed',
unknown = 'unknown',
crashloopbackoff = 'crashloopbackoff',
imagepullbackoff = 'imagepullbackoff',
errimagepull = 'errimagepull',
createcontainerconfigerror = 'createcontainerconfigerror',
containercreating = 'containercreating',
oomkilled = 'oomkilled',
completed = 'completed',
error = 'error',
containercannotrun = 'containercannotrun',
evicted = 'evicted',
nodeaffinity = 'nodeaffinity',
nodelost = 'nodelost',
shutdown = 'shutdown',
unexpectedadmissionerror = 'unexpectedadmissionerror',
no_data = 'no_data',
}
export interface InframonitoringtypesPodRecordDTO {
/**
* @type object,null
@@ -6365,7 +6263,6 @@ export interface InframonitoringtypesPodRecordDTO {
*/
podCPURequest: number;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
/**
* @type number
* @format double
@@ -6382,12 +6279,6 @@ export interface InframonitoringtypesPodRecordDTO {
*/
podMemoryRequest: number;
podPhase: InframonitoringtypesPodPhaseDTO;
/**
* @type integer
* @format int64
*/
podRestarts: number;
podStatus: InframonitoringtypesPodStatusDTO;
/**
* @type string
*/
@@ -6705,7 +6596,6 @@ export interface InframonitoringtypesStatefulSetRecordDTO {
*/
meta: InframonitoringtypesStatefulSetRecordDTOMeta;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
/**
* @type number
* @format double

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#1287b1" viewBox="0 0 24 24"><title>Apache Cassandra</title><path d="M10.374 10.53a3 3 0 0 1-.428-.222l.555.143c0 .02-.01.036-.01.055l-.117.025zm-.283 1.506-.315.253.852-1.079-1.078.391c.002.017.009.033.009.05a.57.57 0 0 1-.184.42q.154.327.375.616a3.2 3.2 0 0 1 .34-.651zm.717-2.347-.652-.82a.43.43 0 0 1-.506.162c-.054.073-.083.162-.13.24l1.258.463zm-1.666.444c-.07.314-.087.637-.05.956a.57.57 0 0 1 .451.475l.946-.606c-.067-.022-.126-.06-.191-.088l-1.119-.08.64-.14a3.2 3.2 0 0 1-.668-.554zM20.1 11.648c-.164.202.833 1.022.833 1.022s-1.654-1.022-2.234-.72c-.278.144.574.811 1.175 1.242-.428-.274-.982-.571-1.175-.408-.328.277 1.565 2.549 1.565 2.549s-2.145-2.322-2.36-2.209c-.214.114.593 1.224.593 1.224s-1.06-1.16-1.35-.959c-.29.202 1.514 3.218 1.514 3.218s-1.956-3.091-2.763-2.574c1.268 2.782.795 3.18.795 3.18s-.162-2.839-1.742-2.764c-.795.038.379 2.12.379 2.12s-1.08-1.902-1.8-1.864c1.326 2.51.854 3.53.854 3.53s.219-2.143-1.58-3.336c.682.606-.427 3.336-.427 3.336s.976-4.023-.719-3.256c-.268.121-.019 2.007-.019 2.007s-.34-2.158-.851-2.045c-.298.066-1.893 2.99-1.893 2.99s1.306-3.16.908-3.027c-.29.096-.833 1.4-.833 1.4s.265-1.287 0-1.363c-.264-.075-1.74 1.363-1.74 1.363s1.097-1.287.908-1.552c-.287-.402-.623-.42-1.022-.265-.581.226-1.363 1.287-1.363 1.287s.78-1.074.643-1.476c-.219-.647-2.46 1.249-2.46 1.249s1.325-1.25 1.022-1.514c-.303-.265-1.947-.183-2.46-.185-1.515-.004-2.039-.36-2.498-.724 1.987.997 3.803-.151 6.094.494l.21.06c-1.3-.558-2.144-1.378-2.226-2.354-.036-.416.074-.827.297-1.222.619-.4 1.29-.773 2.06-1.095a4 4 0 0 0-.064.698c0 2.44 2.203 4.417 4.92 4.417s4.92-1.977 4.92-4.417c0-.45-.083-.881-.223-1.29 1.431.404 2.45.968 3.132 1.335.022.092.045.184.053.279.024.274-.018.547-.11.814.095-.147.198-.288.28-.445.367-.997 1.855.227 1.855.227s-1.085-.454-1.06-.24c.026.215 1.628.96 1.628.96s-1.45-.455-1.362-.114c.088.34 1.817 1.703 1.817 1.703s-1.956-1.489-2.12-1.287zm-7.268 2.65.042-.008-.06.01zM9.256 9.753c.12.13.26.234.396.343l.927-.029-1.064-.788c-.093.154-.195.303-.26.474Zm10.62 3.44c.3.215.54.373.54.373s-.24-.181-.54-.374zM7.507 8.617c-.14.229-.214.492-.215.76a3.99 3.99 0 0 0 2.358 3.64q0-.008.003-.014a3.2 3.2 0 0 1-.58-.788c-.648.099-.926-.794-.336-1.08a3.2 3.2 0 0 1 .138-1.388 3.16 3.16 0 0 1-.52-1.36q-.444.105-.848.23m1.488.82q.163-.36.402-.661a.435.435 0 0 1 .568-.557c.077-.059.166-.099.248-.15a16 16 0 0 0-1.727.284c.114.388.272.76.509 1.084m2.285 3.928c1.4 0 2.633-.723 3.344-1.816a3.4 3.4 0 0 0-1.265-.539l-.297-.023.916.9-1.197-.467.704 1.078-1.074-.832-.012.006.347 1.278-.596-1.134-.098 1.33-.401-1.326-.472 1.261.114-1.359-.015-.008-.814 1.154.286-1.067c-.34.322-.605.713-.781 1.146q.143.153.303.29.484.126 1.008.128m10.145-4.434c.971-.567 1.716-1.955 1.716-1.955s-1.893 1.955-3.205 1.665c1.186-.934 1.766-2.549 1.766-2.549s-1.506 2.325-2.448 2.423c1.086-.959 1.54-2.322 1.54-2.322s-1.237 1.817-2.196 1.944c1.287-1.161 1.338-1.893 1.338-1.893s-1.781 2.302-2.499 1.943c.858-.934 1.439-2.12 1.439-2.12s-1.489 2.019-1.893 1.69c-.277-.05.454-.958.454-.958s-.908.807-1.16.606c.454-.278 1.236-1.64 1.236-1.64S16 7.505 15.621 7.304l.731-1.483s-.73 1.483-1.715 1.23c.454-.58.63-1.112.63-1.112s-.756 1.213-1.69.885c-.22-.077.273-.635.273-.635s-.626.61-1.055.534c-.43-.076.025-.858.025-.858s-.757 1.186-.908 1.136.075-.833.075-.833-.555.908-.858.858 0-.934 0-.934-.328.984-.58.909c-.252-.076-.303-.656-.303-.656s-.068.788-.429.858c-2.725.53-5.728 1.69-9.489 5.45C3.887 10.738 5.3 7.91 11.962 7.659c5.044-.191 7.399 2.137 8.177 2.17C22.51 9.93 24 7.633 24 7.633s-1.489 1.716-2.574 1.3zm-7.74.872-.608.464v.001l.054.003a3.4 3.4 0 0 0 .554-.468m1.583-.426c0-.536-.237-.929-.594-1.217a3.2 3.2 0 0 1-.165.825.393.393 0 0 1-.328.681c-.154.233-.34.445-.549.63l.661.034-.995.237c-.025.018-.045.041-.07.058a3.2 3.2 0 0 1 1.536.691c.32-.574.504-1.235.504-1.94zM10.99 7.996a3.5 3.5 0 0 0-.785.46.43.43 0 0 1-.013.357l.885.643.023-.016-.36-1.262.627 1.12c.018-.006.04-.006.058-.011l-.02-1.251.398 1.163.477-1.15.016 1.268.012.007.713-1.005-.363 1.218.009.01 1.04-.69-.759 1.05.002.005.95-.34.041-.045a.395.395 0 0 1 .394-.632 3.4 3.4 0 0 0 .27-.784 14 14 0 0 0-2.798-.168c-.286.011-.55.033-.817.053"/></svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="48 -2.25 262.5 364"><defs><style>.cls-1{fill:#326ce5}.cls-3{fill:none}</style></defs><path d="M59.724 97.778a10.183 10.183 0 0 1 0-17.073l114.07-74.16a10.18 10.18 0 0 1 11.1 0l114.07 74.16a10.183 10.183 0 0 1 0 17.073l-114.07 74.16a10.18 10.18 0 0 1-11.1 0z" class="cls-1"/><path fill="#c1d2f7" d="M197.356 110.866h7.912a6.003 6.003 0 0 0 5.2-9.005l-25.924-44.902a6.004 6.004 0 0 0-10.399 0l-25.924 44.902a6.003 6.003 0 0 0 5.2 9.005h7.912a6.003 6.003 0 0 1 6.004 6.003v51.257l5.31 3.452a12.29 12.29 0 0 0 13.395 0l5.31-3.452v-51.257a6.003 6.003 0 0 1 6.004-6.003"/><path d="M173.793 353.271a10.1 10.1 0 0 0 3.457 1.402 119 119 0 0 0-6.623-3.46zm-6.456-161.195-11.316 7.356a111 111 0 0 0 11.316 6.6zm24.015 23.716c9.739 3.115 19.813 5.648 30.11 8.186 10.92 2.692 21.973 5.427 32.818 9.009l-35.416-23.025a216 216 0 0 1-27.512-9.41zm0 62.191v.586c0 2.57-2.688 4.652-6.003 4.652H173.34c-3.315 0-6.003-2.083-6.003-4.652v-6.605c-24.906-6.402-49.873-14.485-70.86-33.82l-14.529 9.445c22.468 22.05 50.28 28.933 79.715 36.189 27.178 6.698 55.18 13.651 78.685 33.442l14.68-9.544c-18.532-16.504-40.41-23.687-63.676-29.693M93.315 300.95l38.908 25.295c20.064 5.113 40.2 11.248 58.31 23.36l14.901-9.687c-19.752-13.672-42.55-19.313-66.419-25.197-15.287-3.767-30.835-7.616-45.7-13.771m74.022-82.254a124.3 124.3 0 0 1-21.534-12.62l-14.805 9.625a124.5 124.5 0 0 0 36.339 18.708zm51.41 16.294c-9.106-2.245-18.304-4.524-27.395-7.299v13.682q5.051 1.286 10.177 2.538c29.934 7.379 60.888 15.01 85.708 39.829.61.61 1.162 1.24 1.752 1.857l9.974-6.484a10.1 10.1 0 0 0 3.475-3.8 101 101 0 0 0-3.289-3.486c-22.585-22.585-50.669-29.508-80.402-36.837m-19.933 19.933q-3.722-.918-7.462-1.855v13.21c25.817 6.561 51.803 14.688 73.455 35.04l14.521-9.44-.11-.118c-22.586-22.585-50.67-29.509-80.404-36.837m-31.477-8.587c-16.28-5.278-32.118-12.397-46.344-24.131l-14.682 9.545c17.87 15.557 38.784 22.673 61.026 28.5z" class="cls-3"/><path d="M73.24 254.96c-.348-.348-.658-.71-1-1.06l-12.514 8.136a10 10 0 0 0-1.644 1.397c1.053 1.157 2.115 2.31 3.245 3.44 22.585 22.585 50.67 29.509 80.403 36.837 25.343 6.247 51.401 12.717 73.865 29.603l14.806-9.626c-20.865-16.42-45.53-22.509-71.453-28.898-29.934-7.379-60.888-15.01-85.708-39.83" class="cls-3"/><path d="m218.864 209.962-27.512-17.886v8.476a216 216 0 0 0 27.512 9.41m-27.512 5.83v11.9c9.091 2.774 18.289 5.053 27.395 7.298 29.733 7.329 57.817 14.252 80.402 36.837a101 101 0 0 1 3.29 3.486 10.193 10.193 0 0 0-3.476-13.277l-44.683-29.05c-10.845-3.58-21.898-6.316-32.818-9.008-10.297-2.538-20.371-5.071-30.11-8.186m-24.015-9.76a111 111 0 0 1-11.316-6.6l-10.218 6.644a124.3 124.3 0 0 0 21.534 12.62zm34.192 37.88q-5.125-1.264-10.177-2.539v11.695q3.737.938 7.462 1.855c29.733 7.328 57.818 14.252 80.403 36.837.039.039.073.079.111.118l9.66-6.28c-.589-.618-1.142-1.248-1.751-1.858-24.82-24.82-55.774-32.45-85.708-39.829m-34.192-9.503a124.5 124.5 0 0 1-36.34-18.708l-10.004 6.504c14.226 11.734 30.064 18.853 46.344 24.13zm0 25.842c-22.242-5.828-43.156-12.944-61.026-28.5l-9.834 6.392c20.987 19.336 45.954 27.42 70.86 33.821zm24.015 17.732c23.266 6.006 45.144 13.19 63.675 29.693l9.78-6.358c-21.652-20.352-47.638-28.479-73.455-35.04zM81.948 247.59l-9.707 6.311c.341.35.652.712 1 1.06 24.82 24.82 55.773 32.45 85.707 39.829 25.922 6.39 50.588 12.478 71.452 28.898l9.948-6.467c-23.505-19.791-51.507-26.744-78.685-33.442-29.436-7.256-57.247-14.138-79.715-36.189m-20.621 19.284c-1.13-1.13-2.192-2.283-3.245-3.44a10.154 10.154 0 0 0 1.644 15.68l33.589 21.837c14.865 6.155 30.413 10.004 45.7 13.771 23.869 5.884 46.667 11.525 66.419 25.197l10.16-6.605c-22.463-16.886-48.521-23.356-73.864-29.603-29.733-7.328-57.818-14.252-80.403-36.837m109.3 84.34a119 119 0 0 1 6.623 3.46 10.16 10.16 0 0 0 7.645-1.402l5.639-3.666c-18.11-12.112-38.247-18.247-58.311-23.36z" class="cls-1"/></svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><title>PlanetScale</title><path d="M0 12C0 5.373 5.373 0 12 0c4.873 0 9.067 2.904 10.947 7.077l-15.87 15.87a12 12 0 0 1-1.935-1.099L14.99 12H12l-8.485 8.485A11.96 11.96 0 0 1 0 12m12.004 12L24 12.004C23.998 18.628 18.628 23.998 12.004 24"/></svg>

Before

Width:  |  Height:  |  Size: 326 B

View File

@@ -1,11 +1,10 @@
import { ComponentType } from 'react';
import { TabsProps } from 'antd';
import { History } from 'history';
export type TabRoutes = {
name: React.ReactNode;
route: string;
Component: ComponentType;
Component: () => JSX.Element;
key: string;
};

View File

@@ -177,13 +177,7 @@ export default function ConversationView({
conversationId={conversationId}
messages={messages}
isStreaming={isStreamingHere}
onSendSuggestedPrompt={(text): void => {
handleSend(
text,
undefined,
autoContexts.length > 0 ? autoContexts : undefined,
);
}}
onSendSuggestedPrompt={(text): void => handleSend(text)}
/>
{showDisclaimer && (
<div className={disclaimerClass} role="note" aria-live="polite">

View File

@@ -1,142 +0,0 @@
import { MemoryRouter } from 'react-router-dom';
// eslint-disable-next-line no-restricted-imports
import { fireEvent, render } from '@testing-library/react';
import { MessageContext } from 'api/ai-assistant/chat';
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
import { VariantContext } from 'container/AIAssistant/VariantContext';
const CHIP_ID = 'recent-errors';
const CHIP_TEXT = 'Show me recent errors';
// Auto-derived page context that a normal (typed) send would attach. The chip
// send must forward the exact same array.
const mockAutoContexts: MessageContext[] = [
{
source: 'auto',
type: 'dashboard',
resourceId: 'dashboard-123',
resourceName: 'Checkout dashboard',
},
];
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('container/AIAssistant/getAutoContexts', () => ({
getAutoContexts: jest.fn(() => mockAutoContexts),
}));
jest.mock('container/AIAssistant/hooks/useAIAssistantAnalyticsContext', () => ({
normalizePage: (page: string): string => page,
useAIAssistantAnalyticsContext: (): unknown => ({ threadId: 'thread-1' }),
}));
// ChatInput is heavy and irrelevant here — the chip path lives entirely in the
// empty state. Provide a lightweight stub plus the `autoContextKey` named export
// ConversationView imports for its dismissed-context filter.
jest.mock('container/AIAssistant/components/ChatInput', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="chat-input" />,
autoContextKey: (): string => '',
}));
jest.mock('container/AIAssistant/components/ConversationSkeleton', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="skeleton" />,
}));
// VirtualizedMessages renders the real empty-state chips; stub only its
// never-rendered-in-empty-state children and the virtual list.
jest.mock('components/Noz/Noz', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="noz" />,
}));
jest.mock('container/AIAssistant/components/MessageBubble', () => ({
__esModule: true,
default: (): null => null,
}));
jest.mock('container/AIAssistant/components/StreamingMessage', () => ({
__esModule: true,
default: (): null => null,
}));
jest.mock('react-virtuoso', () => ({
__esModule: true,
Virtuoso: (): null => null,
}));
jest.mock(
'container/AIAssistant/components/VirtualizedMessages/useEmptyStateChips',
() => ({
useEmptyStateChips: (): { chips: { id: string; text: string }[] } => ({
chips: [{ id: CHIP_ID, text: CHIP_TEXT }],
}),
}),
);
// eslint-disable-next-line import/first
import ConversationView from '../ConversationView';
const CONVERSATION_ID = 'conv-1';
function renderView(variant: 'panel' | 'page' | 'modal'): {
getByTestId: (id: string) => HTMLElement;
} {
return render(
<MemoryRouter initialEntries={['/dashboard/dashboard-123']}>
<VariantContext.Provider value={variant}>
<ConversationView conversationId={CONVERSATION_ID} />
</VariantContext.Provider>
</MemoryRouter>,
);
}
describe('ConversationView — empty-state chip context', () => {
let sendMessage: jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
sendMessage = jest.fn();
useAIAssistantStore.setState({
conversations: {
[CONVERSATION_ID]: {
id: CONVERSATION_ID,
messages: [],
createdAt: 1,
updatedAt: 1,
},
},
streams: {},
activeConversationId: CONVERSATION_ID,
isLoadingThread: false,
sendMessage,
} as unknown as Partial<ReturnType<typeof useAIAssistantStore.getState>>);
});
it('forwards the page auto-contexts when a chip is clicked (embedded variant)', () => {
const { getByTestId } = renderView('panel');
fireEvent.click(getByTestId(`empty-state-chip-${CHIP_ID}`));
// The chip send must carry the same auto-contexts a typed message would.
expect(sendMessage).toHaveBeenCalledTimes(1);
expect(sendMessage).toHaveBeenCalledWith(
CHIP_TEXT,
undefined,
mockAutoContexts,
);
});
it('sends undefined contexts on the standalone page (no page context to attach)', () => {
const { getByTestId } = renderView('page');
fireEvent.click(getByTestId(`empty-state-chip-${CHIP_ID}`));
expect(sendMessage).toHaveBeenCalledTimes(1);
expect(sendMessage).toHaveBeenCalledWith(CHIP_TEXT, undefined, undefined);
});
});

View File

@@ -149,18 +149,6 @@
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
padding: 20px 16px 0px 16px;
/* Preserve author-entered line breaks in the description. */
white-space: pre-wrap;
overflow-wrap: anywhere;
a {
color: var(--accent-primary);
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
}
.dashboard-variables {

View File

@@ -43,7 +43,6 @@ import { sortLayout } from 'providers/Dashboard/util';
import { DashboardData } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
import { ROLES, USER_ROLES } from 'types/roles';
import { linkifyText } from 'utils/linkifyText';
import { ComponentTypes } from 'utils/permission';
import { v4 as uuid } from 'uuid';
@@ -516,9 +515,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
</div>
)}
{!isEmpty(description) && (
<section className="dashboard-description-section">
{linkifyText(description ?? '')}
</section>
<section className="dashboard-description-section">{description}</section>
)}
{!isEmpty(dashboardVariables) && (

View File

@@ -1625,9 +1625,6 @@ export const getHostQueryPayload = (
const diskPendingKey = dotMetricsEnabled
? 'system.disk.pending_operations'
: 'system_disk_pending_operations';
const fsUsageKey = dotMetricsEnabled
? 'system.filesystem.usage'
: 'system_filesystem_usage';
return [
{
@@ -2660,155 +2657,6 @@ export const getHostQueryPayload = (
start,
end,
},
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'system_filesystem_usage--float64--Gauge--true',
key: fsUsageKey,
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: true,
expression: 'A',
filters: {
items: [
{
id: 'fs_f1',
key: {
dataType: DataTypes.String,
id: 'host_name--string--tag--false',
key: hostNameKey,
type: 'tag',
},
op: '=',
value: hostName,
},
{
id: 'fs_f2',
key: {
dataType: DataTypes.String,
id: 'state--string--tag--false',
key: 'state',
type: 'tag',
},
op: '=',
value: 'used',
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'mountpoint--string--tag--false',
key: 'mountpoint',
type: 'tag',
},
],
having: [
{
columnName: `SUM(${fsUsageKey})`,
op: '>',
value: 0,
},
],
legend: '{{mountpoint}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: ReduceOperators.AVG,
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'system_filesystem_usage--float64--Gauge--true',
key: fsUsageKey,
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: true,
expression: 'B',
filters: {
items: [
{
id: 'fs_f3',
key: {
dataType: DataTypes.String,
id: 'host_name--string--tag--false',
key: hostNameKey,
type: 'tag',
},
op: '=',
value: hostName,
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'mountpoint--string--tag--false',
key: 'mountpoint',
type: 'tag',
},
],
having: [
{
columnName: `SUM(${fsUsageKey})`,
op: '>',
value: 0,
},
],
legend: '{{mountpoint}}',
limit: null,
orderBy: [],
queryName: 'B',
reduceTo: ReduceOperators.AVG,
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
],
queryFormulas: [
{
disabled: false,
expression: 'A/B',
legend: '{{mountpoint}}',
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
queryType: EQueryType.QUERY_BUILDER,
},
variables: {},
formatForWeb: false,
start,
end,
},
];
};
@@ -2884,5 +2732,4 @@ export const hostWidgetInfo = [
{ title: 'System disk operations/s', yAxisUnit: 'short' },
{ title: 'Queue size', yAxisUnit: 'short' },
{ title: 'System disk operation time/s', yAxisUnit: 's' },
{ title: 'Disk Usage (%) by mountpoint', yAxisUnit: 'percentunit' },
];

View File

@@ -20,7 +20,6 @@ import azureOpenaiUrl from '@/assets/Logos/azure-openai.svg';
import azureSqlDatabaseMetricsUrl from '@/assets/Logos/azure-sql-database-metrics.svg';
import azureVmUrl from '@/assets/Logos/azure-vm.svg';
import basetenUrl from '@/assets/Logos/baseten.svg';
import cassandraUrl from '@/assets/Logos/cassandra.svg';
import celeryUrl from '@/assets/Logos/celery.svg';
import certManagerUrl from '@/assets/Logos/cert-manager.svg';
import claudeCodeUrl from '@/assets/Logos/claude-code.svg';
@@ -52,7 +51,6 @@ import externalApiMonitoringUrl from '@/assets/Logos/external-api-monitoring.svg
import fluentbitUrl from '@/assets/Logos/fluentbit.svg';
import fluentdUrl from '@/assets/Logos/fluentd.svg';
import flutterMonitoringUrl from '@/assets/Logos/flutter-monitoring.svg';
import fluxcdUrl from '@/assets/Logos/fluxcd.svg';
import flyIoUrl from '@/assets/Logos/fly-io.svg';
import fromLogFileUrl from '@/assets/Logos/from-log-file.svg';
import gcpAppEngineUrl from '@/assets/Logos/gcp-app-engine.svg';
@@ -96,7 +94,6 @@ import langtraceUrl from '@/assets/Logos/langtrace.svg';
import litellmUrl from '@/assets/Logos/litellm.svg';
import livekitUrl from '@/assets/Logos/livekit.svg';
import llamaindexUrl from '@/assets/Logos/llamaindex.svg';
import llmMonitoringUrl from '@/assets/Logos/llm-monitoring.svg';
import logrusUrl from '@/assets/Logos/logrus.svg';
import logsUrl from '@/assets/Logos/logs.svg';
import logstashUrl from '@/assets/Logos/logstash.svg';
@@ -123,7 +120,6 @@ import opentelemetryUrl from '@/assets/Logos/opentelemetry.svg';
import phpUrl from '@/assets/Logos/php.svg';
import pinoUrl from '@/assets/Logos/pino.svg';
import pipecatUrl from '@/assets/Logos/pipecat.svg';
import planetscaleUrl from '@/assets/Logos/planetscale.svg';
import postgresqlUrl from '@/assets/Logos/postgresql.svg';
import prometheusUrl from '@/assets/Logos/prometheus.svg';
import pydanticAiUrl from '@/assets/Logos/pydantic-ai.svg';
@@ -1530,28 +1526,6 @@ const onboardingConfigWithLinks = [
id: 'nginx-tracing',
link: '/docs/instrumentation/opentelemetry-nginx/',
},
{
dataSource: 'nginx-ingress-controller',
label: 'NGINX Ingress Controller',
imgUrl: nginxUrl,
tags: ['infrastructure monitoring'],
module: 'metrics',
relatedSearchKeywords: [
'ingress',
'ingress controller',
'kubernetes ingress',
'monitoring',
'nginx ingress',
'nginx ingress controller',
'nginx ingress metrics',
'nginx ingress monitoring',
'nginx ingress observability',
'observability',
'opentelemetry nginx ingress',
],
id: 'nginx-ingress-controller',
link: '/docs/metrics-management/nginx-ingress-controller/',
},
{
dataSource: 'opentelemetry-cloudflare',
label: 'Cloudflare Tracing',
@@ -1612,119 +1586,6 @@ const onboardingConfigWithLinks = [
id: 'opentelemetry-cloudflare-logs',
link: '/docs/logs-management/send-logs/cloudflare-logs/',
},
{
dataSource: 'cloudflare-workers',
label: 'Cloudflare Workers',
imgUrl: cloudflareUrl,
tags: ['apm/traces'],
module: 'apm',
relatedSearchKeywords: [
'cloudflare',
'cloudflare workers',
'cloudflare workers monitoring',
'cloudflare workers observability',
'cloudflare workers otlp',
'edge computing monitoring',
'monitor cloudflare workers',
'monitoring',
'observability',
'opentelemetry cloudflare workers',
'otlp',
'serverless monitoring',
],
id: 'cloudflare-workers',
link: '/docs/integrations/outposts/cloudflare-workers/',
},
{
dataSource: 'opentelemetry-cassandra',
label: 'Cassandra',
imgUrl: cassandraUrl,
tags: ['database'],
module: 'apm',
relatedSearchKeywords: [
'apache cassandra',
'cassandra',
'cassandra database',
'cassandra logs',
'cassandra metrics',
'cassandra monitoring',
'cassandra observability',
'database',
'monitoring',
'nosql',
'observability',
'opentelemetry cassandra',
],
id: 'opentelemetry-cassandra',
link: '/docs/integrations/opentelemetry-cassandra/',
},
{
dataSource: 'fluxcd',
label: 'FluxCD',
imgUrl: fluxcdUrl,
tags: ['infrastructure monitoring'],
module: 'metrics',
relatedSearchKeywords: [
'continuous delivery',
'flux',
'fluxcd',
'fluxcd dashboard',
'fluxcd metrics',
'fluxcd monitoring',
'fluxcd observability',
'gitops',
'kubernetes',
'monitoring',
'observability',
'opentelemetry fluxcd',
],
id: 'fluxcd',
link: '/docs/metrics-management/fluxcd-metrics/',
},
{
dataSource: 'planetscale',
label: 'PlanetScale',
imgUrl: planetscaleUrl,
tags: ['database'],
module: 'apm',
relatedSearchKeywords: [
'database',
'monitoring',
'mysql',
'observability',
'opentelemetry planetscale',
'planetscale',
'planetscale database',
'planetscale metrics',
'planetscale monitoring',
'planetscale observability',
'serverless database',
],
id: 'planetscale',
link: '/docs/metrics-management/opentelemetry-planetscale/',
},
{
dataSource: 'hermes-agent',
label: 'Hermes Agent',
imgUrl: llmMonitoringUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'ai agent monitoring',
'hermes',
'hermes agent',
'hermes agent monitoring',
'hermes agent observability',
'hermes monitoring',
'llm monitoring',
'monitoring',
'nous research',
'observability',
'opentelemetry hermes',
],
id: 'hermes-agent',
link: '/docs/hermes-monitoring/',
},
{
dataSource: 'convex-logs',
label: 'Convex Logs',

View File

@@ -1,21 +0,0 @@
# AuthZ
Permission-based authorization system for SigNoz frontend.
## Supported Resources
See [hooks/useAuthZ/permissions.type.ts](./hooks/useAuthZ/permissions.type.ts) for available resources and verbs.
If your page/content represents a resource not listed there, skip authz implementation — the backend doesn't enforce it yet.
## UI Gating
Need to gate UI based on permissions? See [components/README.md](./components/README.md).
Covers: AuthZButton, AuthZTooltip, withAuthZ*, AuthZGuard*, when to use each.
## Testing
Need to test authz behavior? See [utils/README.md](./utils/README.md).
Covers: MSW handlers, mock hooks, test patterns.

View File

@@ -1,81 +0,0 @@
import { ReactElement } from 'react';
import { render, screen } from 'tests/test-utils';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import type { AuthZObject } from 'lib/authz/hooks/useAuthZ/types';
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
import AuthZButton from './AuthZButton';
// AuthZButton is a thin composition over AuthZTooltip + Button. The denial
// tooltip / disabled-on-deny UX is owned and tested by AuthZTooltip; here we
// assert AuthZButton forwards the right props and renders a Button child.
jest.mock('lib/authz/components/AuthZTooltip/AuthZTooltip');
const mockTooltip = AuthZTooltip as unknown as jest.Mock;
const createPerm = buildPermission(
'create',
'serviceaccount:*' as AuthZObject<'create'>,
);
describe('AuthZButton', () => {
beforeEach(() => {
mockTooltip.mockImplementation(
({ children }: { children: ReactElement }) => children,
);
});
afterEach(() => {
mockTooltip.mockReset();
});
it('renders a Button child with forwarded props', () => {
render(
<AuthZButton checks={[createPerm]} testId="create-btn">
Create
</AuthZButton>,
);
expect(screen.getByTestId('create-btn')).toBeInTheDocument();
expect(screen.getByTestId('create-btn').tagName).toBe('BUTTON');
});
it('forwards checks and enables the check by default', () => {
render(
<AuthZButton checks={[createPerm]} testId="create-btn">
Create
</AuthZButton>,
);
expect(mockTooltip).toHaveBeenCalledTimes(1);
expect(mockTooltip.mock.calls[0][0]).toMatchObject({
checks: [createPerm],
enabled: true,
});
});
it('forwards a custom tooltipMessage', () => {
render(
<AuthZButton
checks={[createPerm]}
tooltipMessage="Ask an admin"
testId="create-btn"
>
Create
</AuthZButton>,
);
expect(mockTooltip.mock.calls[0][0]).toMatchObject({
tooltipMessage: 'Ask an admin',
});
});
it('passes authZEnabled through as the tooltip enabled flag', () => {
render(
<AuthZButton checks={[createPerm]} authZEnabled={false} testId="create-btn">
Create
</AuthZButton>,
);
expect(mockTooltip.mock.calls[0][0]).toMatchObject({ enabled: false });
});
});

View File

@@ -1,37 +0,0 @@
import { Button, ButtonProps } from '@signozhq/ui/button';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import type { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
export type AuthZButtonProps = ButtonProps & {
/**
* Permissions required to enable the button (AND semantics).
*/
checks: BrandedPermission[];
/**
* Override the default denial tooltip message.
*/
tooltipMessage?: string;
/**
* Gate the permission check itself. When false, renders a plain button.
*/
authZEnabled?: boolean;
};
function AuthZButton({
checks,
tooltipMessage,
authZEnabled = true,
...buttonProps
}: AuthZButtonProps): JSX.Element {
return (
<AuthZTooltip
checks={checks}
enabled={authZEnabled}
tooltipMessage={tooltipMessage}
>
<Button {...buttonProps} />
</AuthZTooltip>
);
}
export default AuthZButton;

View File

@@ -1,202 +0,0 @@
import { render, screen, waitFor } from 'tests/test-utils';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import {
AUTHZ_CHECK_URL,
setupAuthzAllow,
setupAuthzDeny,
} from 'lib/authz/utils/authz-test-utils';
import type { AuthZObject } from 'lib/authz/hooks/useAuthZ/types';
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
import { AuthZGuard } from './AuthZGuard';
import { AuthZGuardContent } from './AuthZGuardContent';
import { AuthZGuardPage } from './AuthZGuardPage';
const readPerm = buildPermission('read', 'role:*' as AuthZObject<'read'>);
const Protected = (): JSX.Element => <div>Protected content</div>;
describe('AuthZGuard', () => {
it('renders children when allowed', async () => {
server.use(setupAuthzAllow(readPerm));
render(
<AuthZGuard checks={[readPerm]}>
<Protected />
</AuthZGuard>,
);
await waitFor(() => {
expect(screen.getByText('Protected content')).toBeInTheDocument();
});
});
it('renders the fallback when denied', async () => {
server.use(setupAuthzDeny(readPerm));
render(
<AuthZGuard checks={[readPerm]} fallback={<div>No access</div>}>
<Protected />
</AuthZGuard>,
);
await waitFor(() => {
expect(screen.getByText('No access')).toBeInTheDocument();
});
expect(screen.queryByText('Protected content')).not.toBeInTheDocument();
});
it('passes denied permissions to a function fallback', async () => {
server.use(setupAuthzDeny(readPerm));
render(
<AuthZGuard
checks={[readPerm]}
fallback={({ deniedPermissions }): JSX.Element => (
<div>denied: {deniedPermissions.length}</div>
)}
>
<Protected />
</AuthZGuard>,
);
await waitFor(() => {
expect(screen.getByText('denied: 1')).toBeInTheDocument();
});
});
it('renders nothing for a denied check with no fallback', async () => {
server.use(setupAuthzDeny(readPerm));
const { container } = render(
<AuthZGuard checks={[readPerm]}>
<Protected />
</AuthZGuard>,
);
await waitFor(() => {
expect(screen.queryByText('Protected content')).not.toBeInTheDocument();
});
expect(container).toBeEmptyDOMElement();
});
it('renders the loading fallback while checking', () => {
server.use(setupAuthzAllow(readPerm));
render(
<AuthZGuard checks={[readPerm]} fallbackOnLoading={<div>Loading</div>}>
<Protected />
</AuthZGuard>,
);
expect(screen.getByText('Loading…')).toBeInTheDocument();
});
it('fails open on error by default (renders children)', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) =>
res(ctx.status(500), ctx.json({ error: 'boom' })),
),
);
render(
<AuthZGuard checks={[readPerm]} fallback={<div>No access</div>}>
<Protected />
</AuthZGuard>,
);
await waitFor(() => {
expect(screen.getByText('Protected content')).toBeInTheDocument();
});
});
it('renders the fallback on error when failOpenOnError is false', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) =>
res(ctx.status(500), ctx.json({ error: 'boom' })),
),
);
render(
<AuthZGuard
checks={[readPerm]}
onFailRenderContent={false}
fallback={<div>No access</div>}
>
<Protected />
</AuthZGuard>,
);
await waitFor(() => {
expect(screen.getByText('No access')).toBeInTheDocument();
});
expect(screen.queryByText('Protected content')).not.toBeInTheDocument();
});
});
describe('AuthZGuardPage', () => {
it('renders the full-page denied screen when denied', async () => {
server.use(setupAuthzDeny(readPerm));
render(
<AuthZGuardPage checks={[readPerm]}>
<Protected />
</AuthZGuardPage>,
);
await waitFor(() => {
expect(
screen.getByText('Uh-oh! You are not authorized'),
).toBeInTheDocument();
});
expect(screen.getByText('read:role:*')).toBeInTheDocument();
});
it('renders the app loader while checking', () => {
server.use(setupAuthzDeny(readPerm));
render(
<AuthZGuardPage checks={[readPerm]}>
<Protected />
</AuthZGuardPage>,
);
expect(
screen.getByText(
'OpenTelemetry-Native Logs, Metrics and Traces in a single pane',
),
).toBeInTheDocument();
});
});
describe('AuthZGuardContent', () => {
it('renders the denied callout when denied', async () => {
server.use(setupAuthzDeny(readPerm));
render(
<AuthZGuardContent checks={[readPerm]}>
<Protected />
</AuthZGuardContent>,
);
await waitFor(() => {
expect(screen.getByText('read:role:*')).toBeInTheDocument();
});
expect(screen.queryByText('Protected content')).not.toBeInTheDocument();
});
it('renders children when allowed', async () => {
server.use(setupAuthzAllow(readPerm));
render(
<AuthZGuardContent checks={[readPerm]}>
<Protected />
</AuthZGuardContent>,
);
await waitFor(() => {
expect(screen.getByText('Protected content')).toBeInTheDocument();
});
});
});

View File

@@ -1,82 +0,0 @@
import { ReactElement, ReactNode, useMemo } from 'react';
import type { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
export type AuthZGuardFallback =
| ReactNode
| ((info: { deniedPermissions: BrandedPermission[] }) => ReactNode);
export type AuthZGuardProps = {
/**
* Permissions required to render `children` (AND semantics).
*/
checks: BrandedPermission[];
children: ReactElement;
/**
* Rendered when denied. A function receives the denied permissions.
*/
fallback?: AuthZGuardFallback;
fallbackOnLoading?: ReactNode;
/**
* By default, we don't expect the check API request to fail, in those cases, we prefer to show the content and then let the API fail (during list/create).
*
* In case you want to have a different behavior when request fail, set to false.
*
* @default true
*/
onFailRenderContent?: boolean;
};
function resolveFallback(
fallback: AuthZGuardFallback | undefined,
deniedPermissions: BrandedPermission[],
): ReactNode {
if (typeof fallback === 'function') {
return fallback({ deniedPermissions });
}
return fallback ?? null;
}
export function AuthZGuard({
checks,
children,
fallback,
fallbackOnLoading,
onFailRenderContent = true,
}: AuthZGuardProps): JSX.Element | null {
const { isLoading, error, permissions } = useAuthZ(checks);
// TODO(authz): Use allowed/deniedPermissions from useAuthZ after devtools PR merges
const { allowed, deniedPermissions } = useMemo(() => {
if (!permissions) {
return { allowed: false, deniedPermissions: [] as BrandedPermission[] };
}
const denied = Object.entries(permissions)
.filter(([, { isGranted }]) => !isGranted)
.map(([perm]) => perm as BrandedPermission);
return {
allowed: denied.length === 0,
deniedPermissions: denied,
};
}, [permissions]);
if (isLoading) {
return <>{fallbackOnLoading ?? null}</>;
}
if (error) {
return onFailRenderContent ? (
children
) : (
<>{resolveFallback(fallback, deniedPermissions)}</>
);
}
if (!allowed) {
return <>{resolveFallback(fallback, deniedPermissions)}</>;
}
return children;
}

View File

@@ -1,21 +0,0 @@
import { ReactElement } from 'react';
import PermissionDeniedCallout from 'lib/authz/components/PermissionDeniedCallout/PermissionDeniedCallout';
import { AuthZGuard, AuthZGuardProps } from './AuthZGuard';
export function AuthZGuardContent({
fallback,
...rest
}: AuthZGuardProps): JSX.Element | null {
return (
<AuthZGuard
{...rest}
fallback={
fallback ??
(({ deniedPermissions }): ReactElement => (
<PermissionDeniedCallout deniedPermissions={deniedPermissions} />
))
}
/>
);
}

View File

@@ -1,24 +0,0 @@
import { ReactElement } from 'react';
import AppLoading from 'components/AppLoading/AppLoading';
import PermissionDeniedFullPage from 'lib/authz/components/PermissionDeniedFullPage/PermissionDeniedFullPage';
import { AuthZGuard, AuthZGuardProps } from './AuthZGuard';
export function AuthZGuardPage({
fallback,
fallbackOnLoading,
...rest
}: AuthZGuardProps): JSX.Element | null {
return (
<AuthZGuard
{...rest}
fallbackOnLoading={fallbackOnLoading ?? <AppLoading />}
fallback={
fallback ??
(({ deniedPermissions }): ReactElement => (
<PermissionDeniedFullPage deniedPermissions={deniedPermissions} />
))
}
/>
);
}

View File

@@ -16,8 +16,6 @@ const noPermissions = {
isFetching: false,
error: null,
permissions: null,
allowed: false,
deniedPermissions: [] as BrandedPermission[],
refetchPermissions: jest.fn(),
};
@@ -162,11 +160,11 @@ describe('AuthZTooltip — multi-check (checks array)', () => {
</AuthZTooltip>,
);
const button = screen.getByRole('button', { name: 'Action' });
expect(button).toBeDisabled();
expect(screen.getByRole('button', { name: 'Action' })).toBeDisabled();
expect(button.getAttribute('data-denied-permissions')).toContain(sa);
expect(button.getAttribute('data-denied-permissions')).toContain(
const wrapper = screen.getByRole('button', { name: 'Action' }).parentElement;
expect(wrapper?.getAttribute('data-denied-permissions')).toContain(sa);
expect(wrapper?.getAttribute('data-denied-permissions')).toContain(
attachRolePerm,
);
});

View File

@@ -1,3 +1,7 @@
.wrapper {
cursor: not-allowed;
}
.errorContent {
background: var(--callout-error-background) !important;
border-color: var(--callout-error-border) !important;

View File

@@ -1,4 +1,4 @@
import { CSSProperties, ReactElement, cloneElement, useMemo } from 'react';
import { ReactElement, cloneElement, useMemo } from 'react';
import {
TooltipRoot,
TooltipContent,
@@ -11,13 +11,6 @@ import { formatPermission } from 'lib/authz/hooks/useAuthZ/utils';
import { useAppContext } from 'providers/App/App';
import styles from './AuthZTooltip.module.scss';
const DISABLED_STYLE: CSSProperties = {
pointerEvents: 'all',
cursor: 'not-allowed',
};
const noOp = (): void => {};
interface AuthZTooltipProps {
checks: BrandedPermission[];
children: ReactElement;
@@ -56,13 +49,11 @@ function AuthZTooltip({
}, [checks, permissions]);
if (shouldCheck && isLoading) {
return cloneElement(children, {
disabled: true,
style: DISABLED_STYLE,
onClick: noOp,
onMouseDown: noOp,
onPointerDown: noOp,
});
return (
<span className={styles.wrapper}>
{cloneElement(children, { disabled: true })}
</span>
);
}
if (!shouldCheck || deniedPermissions.length === 0) {
@@ -73,14 +64,12 @@ function AuthZTooltip({
<TooltipProvider>
<TooltipRoot>
<TooltipTrigger asChild>
{cloneElement(children, {
disabled: true,
style: DISABLED_STYLE,
onClick: noOp,
onMouseDown: noOp,
onPointerDown: noOp,
'data-denied-permissions': deniedPermissions.join(','),
})}
<span
className={styles.wrapper}
data-denied-permissions={deniedPermissions.join(',')}
>
{cloneElement(children, { disabled: true })}
</span>
</TooltipTrigger>
<TooltipContent className={styles.errorContent}>
{formatDeniedMessage(deniedPermissions, user.id, tooltipMessage)}

View File

@@ -0,0 +1,263 @@
import { ReactElement } from 'react';
import { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, waitFor } from 'tests/test-utils';
import {
AUTHZ_CHECK_URL,
authzMockResponse,
} from 'lib/authz/utils/authz-test-utils';
import { GuardAuthZ } from './GuardAuthZ';
describe('GuardAuthZ', () => {
const TestChild = (): ReactElement => <div>Protected Content</div>;
const LoadingFallback = (): ReactElement => <div>Loading...</div>;
const NoPermissionFallback = (_response: {
requiredPermissionName: BrandedPermission;
}): ReactElement => <div>Access denied</div>;
const NoPermissionFallbackWithSuggestions = (response: {
requiredPermissionName: BrandedPermission;
}): ReactElement => (
<div>
Access denied. Required permission: {response.requiredPermissionName}
</div>
);
it('should render children when permission is granted', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
render(
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
});
it('should render fallbackOnLoading when loading', () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (_req, res, ctx) => {
return res(
ctx.delay('infinite'),
ctx.status(200),
ctx.json({ data: [], status: 'success' }),
);
}),
);
render(
<GuardAuthZ
relation="read"
object="role:*"
fallbackOnLoading={<LoadingFallback />}
>
<TestChild />
</GuardAuthZ>,
);
expect(screen.getByText('Loading...')).toBeInTheDocument();
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render null when loading and no fallbackOnLoading provided', () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (_req, res, ctx) => {
return res(
ctx.delay('infinite'),
ctx.status(200),
ctx.json({ data: [], status: 'success' }),
);
}),
);
const { container } = render(
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
expect(container.firstChild).toBeNull();
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render children when API error occurs and no fallbackOnError provided (fail open)', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
render(
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
});
it('should render fallbackOnError when API error occurs and fallbackOnError is provided', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
render(
<GuardAuthZ
relation="read"
object="role:*"
fallbackOnError={<div>Custom error fallback</div>}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Custom error fallback')).toBeInTheDocument();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render fallbackOnNoPermissions when permission is denied', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
}),
);
render(
<GuardAuthZ
relation="update"
object="role:123"
fallbackOnNoPermissions={NoPermissionFallback}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Access denied')).toBeInTheDocument();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render null when permission is denied and no fallbackOnNoPermissions provided', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
}),
);
const { container } = render(
<GuardAuthZ relation="update" object="role:123">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(container.firstChild).toBeNull();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render null when permissions object is null', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json({ data: [], status: 'success' }));
}),
);
const { container } = render(
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(container.firstChild).toBeNull();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should pass requiredPermissionName to fallbackOnNoPermissions', async () => {
const permission = buildPermission('update', 'role:123');
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
}),
);
render(
<GuardAuthZ
relation="update"
object="role:123"
fallbackOnNoPermissions={NoPermissionFallbackWithSuggestions}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(
screen.getByText(/Access denied. Required permission:/),
).toBeInTheDocument();
});
expect(
screen.getAllByText(
new RegExp(permission.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
).length,
).toBeGreaterThan(0);
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should handle different relation and object combinations', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const { rerender } = render(
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
rerender(
<GuardAuthZ relation="delete" object="role:456">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,50 @@
import { ReactElement } from 'react';
import {
AuthZObject,
AuthZRelation,
BrandedPermission,
} from 'lib/authz/hooks/useAuthZ/types';
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
export type GuardAuthZProps<R extends AuthZRelation> = {
children: ReactElement;
relation: R;
object: AuthZObject<R>;
fallbackOnLoading?: JSX.Element;
fallbackOnError?: JSX.Element;
fallbackOnNoPermissions?: (response: {
requiredPermissionName: BrandedPermission;
}) => JSX.Element;
};
export function GuardAuthZ<R extends AuthZRelation>({
children,
relation,
object,
fallbackOnLoading,
fallbackOnError,
fallbackOnNoPermissions,
}: GuardAuthZProps<R>): JSX.Element | null {
const permission = buildPermission<R>(relation, object);
const { permissions, isLoading, error } = useAuthZ([permission]);
if (isLoading) {
return fallbackOnLoading ?? null;
}
if (error) {
return fallbackOnError ?? children;
}
if (!permissions?.[permission]?.isGranted) {
return (
fallbackOnNoPermissions?.({
requiredPermissionName: permission,
}) ?? null
);
}
return children;
}

View File

@@ -1,39 +1,18 @@
import { render, screen } from 'tests/test-utils';
import PermissionDeniedCallout from './PermissionDeniedCallout';
import {
buildPermission,
buildObjectString,
} from 'lib/authz/hooks/useAuthZ/utils';
describe('PermissionDeniedCallout', () => {
it('renders the permission name in the callout message', () => {
const deniedPermissions = [
buildPermission('read', buildObjectString('serviceaccount', '*')),
];
render(<PermissionDeniedCallout deniedPermissions={deniedPermissions} />);
render(<PermissionDeniedCallout permissionName="serviceaccount:attach" />);
expect(screen.getByText(/is not authorized/)).toBeInTheDocument();
expect(screen.getByText(/read:serviceaccount:\*/)).toBeInTheDocument();
});
it('renders multiple denied permissions', () => {
const deniedPermissions = [
buildPermission('read', buildObjectString('serviceaccount', '*')),
buildPermission('update', buildObjectString('role', 'admin')),
];
render(<PermissionDeniedCallout deniedPermissions={deniedPermissions} />);
expect(screen.getByText(/read:serviceaccount:\*/)).toBeInTheDocument();
expect(screen.getByText(/update:role:admin/)).toBeInTheDocument();
expect(screen.getByText(/serviceaccount:attach/)).toBeInTheDocument();
});
it('accepts an optional className', () => {
const deniedPermissions = [
buildPermission('read', buildObjectString('serviceaccount', '*')),
];
const { container } = render(
<PermissionDeniedCallout
deniedPermissions={deniedPermissions}
permissionName="serviceaccount:read"
className="custom-class"
/>,
);

View File

@@ -3,32 +3,18 @@ import cx from 'classnames';
import styles from './PermissionDeniedCallout.module.scss';
import { useAppContext } from 'providers/App/App';
import { Typography } from '@signozhq/ui/typography';
import { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
import { formatPermission } from 'lib/authz/hooks/useAuthZ/utils';
export interface PermissionDeniedCalloutProps {
/**
* @deprecated Use `deniedPermissions` instead. Will be removed after authz devtools PR merges.
*/
permissionName?: string;
deniedPermissions?: BrandedPermission[];
interface PermissionDeniedCalloutProps {
permissionName: string;
className?: string;
}
function PermissionDeniedCallout({
permissionName,
deniedPermissions,
className,
}: PermissionDeniedCalloutProps): JSX.Element {
const { user } = useAppContext();
// TODO(authz): Remove permissionName support after devtools PR merges
const formattedPermissions = deniedPermissions
? deniedPermissions.map(formatPermission)
: permissionName
? [permissionName]
: [];
return (
<Callout
type="error"
@@ -39,12 +25,7 @@ function PermissionDeniedCallout({
<Typography.Text className={styles.permission}>
<code className={styles.permissionCode}>user/{user.id}</code> is not
authorized to perform{' '}
{formattedPermissions.map((perm, idx) => (
<span key={perm}>
<code className={styles.permissionCode}>{perm}</code>
{idx < formattedPermissions.length - 1 && ', '}
</span>
))}
<code className={styles.permissionCode}>{permissionName}</code>
</Typography.Text>
</Callout>
);

View File

@@ -1,29 +1,17 @@
import { render, screen } from 'tests/test-utils';
import PermissionDeniedFullPage from './PermissionDeniedFullPage';
import {
buildPermission,
buildObjectString,
} from 'lib/authz/hooks/useAuthZ/utils';
describe('PermissionDeniedFullPage', () => {
it('renders the title and subtitle with the permissionName interpolated', () => {
const deniedPermissions = [
buildPermission('read', buildObjectString('serviceaccount', '*')),
];
render(<PermissionDeniedFullPage deniedPermissions={deniedPermissions} />);
render(<PermissionDeniedFullPage permissionName="serviceaccount:list" />);
expect(screen.getByText('Uh-oh! You are not authorized')).toBeInTheDocument();
expect(screen.getByText(/read:serviceaccount:\*/)).toBeInTheDocument();
expect(screen.getByText(/serviceaccount:list/)).toBeInTheDocument();
expect(screen.getByText(/is not authorized to perform/)).toBeInTheDocument();
});
it('renders with multiple denied permissions', () => {
const deniedPermissions = [
buildPermission('read', buildObjectString('role', 'admin')),
buildPermission('update', buildObjectString('role', 'admin')),
];
render(<PermissionDeniedFullPage deniedPermissions={deniedPermissions} />);
expect(screen.getByText(/read:role:admin/)).toBeInTheDocument();
expect(screen.getByText(/update:role:admin/)).toBeInTheDocument();
it('renders with a different permissionName', () => {
render(<PermissionDeniedFullPage permissionName="role:read" />);
expect(screen.getByText(/role:read/)).toBeInTheDocument();
});
});

View File

@@ -3,30 +3,16 @@ import { CircleSlash2 } from '@signozhq/icons';
import styles from './PermissionDeniedFullPage.module.scss';
import { Style } from '@signozhq/design-tokens';
import { useAppContext } from 'providers/App/App';
import { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
import { formatPermission } from 'lib/authz/hooks/useAuthZ/utils';
export interface PermissionDeniedFullPageProps {
/**
* @deprecated Use `deniedPermissions` instead. Will be removed after authz devtools PR merges.
*/
permissionName?: string;
deniedPermissions?: BrandedPermission[];
interface PermissionDeniedFullPageProps {
permissionName: string;
}
function PermissionDeniedFullPage({
permissionName,
deniedPermissions,
}: PermissionDeniedFullPageProps): JSX.Element {
const { user } = useAppContext();
// TODO(authz): Remove permissionName support after devtools PR merges
const formattedPermissions = deniedPermissions
? deniedPermissions.map(formatPermission)
: permissionName
? [permissionName]
: [];
return (
<div className={styles.container}>
<div className={styles.content}>
@@ -36,13 +22,7 @@ function PermissionDeniedFullPage({
<p className={styles.title}>Uh-oh! You are not authorized</p>
<p className={styles.subtitle}>
<code className={styles.permission}>user/{user.id}</code> is not authorized
to perform{' '}
{formattedPermissions.map((perm, idx) => (
<span key={perm}>
<code className={styles.permission}>{perm}</code>
{idx < formattedPermissions.length - 1 && ', '}
</span>
))}
to perform <code className={styles.permission}>{permissionName}</code>
</p>
</div>
</div>

View File

@@ -1,185 +0,0 @@
# AuthZ Components
Quick reference for permission-gating UI. All components use AND semantics: user needs ALL permissions in `checks` array.
## Decision Tree
```
Need to gate...
├── A button? → AuthZButton
├── Any element with tooltip on deny? → AuthZTooltip
├── A section inside a page? → withAuthZContent (preferred)
│ └── Need JSX wrapper? → AuthZGuardContent
├── An entire page/route? → withAuthZPage (preferred)
│ └── Need JSX wrapper? → AuthZGuardPage
├── Need full control over fallback? → withAuthZ / AuthZGuard
└── None of above fit?
├── Can create wrapper component? → Create it (like AuthZButton)
└── Last resort → useAuthZ hook directly
```
## Building Permissions
Use `buildPermission`, `buildObjectString` or pre-built constants. Never cast with `as BrandedPermission`.
```tsx
import { buildPermission, buildObjectString } from 'lib/authz/hooks/useAuthZ/utils';
import {
RoleCreatePermission,
buildRoleReadPermission
} from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
// Static permission (pre-built)
const checks = [RoleCreatePermission];
// Dynamic permission (builder fn)
const checks = [buildRoleReadPermission(roleId)];
// Custom permission (buildPermission + buildObjectString)
const checks = [buildPermission('read', buildObjectString('dashboard', dashboardId))];
```
## Creating Permission Helpers
When adding authz to a new resource, create a permissions file under `lib/authz/hooks/useAuthZ/permissions/`.
```tsx
// lib/authz/hooks/useAuthZ/permissions/dashboard.permissions.ts
import { buildPermission } from '../utils';
import type { BrandedPermission } from '../types';
// Collection-level — wildcard, no specific id needed
export const DashboardCreatePermission = buildPermission('create', 'dashboard:*');
export const DashboardListPermission = buildPermission('list', 'dashboard:*');
// Resource-level — require specific id
export const buildDashboardReadPermission = (id: string): BrandedPermission =>
buildPermission('read', `dashboard:${id}`);
export const buildDashboardUpdatePermission = (id: string): BrandedPermission =>
buildPermission('update', `dashboard:${id}`);
export const buildDashboardDeletePermission = (id: string): BrandedPermission =>
buildPermission('delete', `dashboard:${id}`);
```
Pattern:
- `<Resource><Action>Permission` → collection-level const (wildcard `*`)
- `build<Resource><Action>Permission(id)` → resource-level fn (specific id)
## Components
### AuthZButton
Button that disables + shows tooltip when denied.
```tsx
import { SACreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
<AuthZButton checks={[SACreatePermission]} onClick={handleCreate}>
Create
</AuthZButton>
```
### AuthZTooltip
Wraps any element. Disables child + shows denial tooltip.
```tsx
import { buildSADeletePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
<AuthZTooltip checks={[buildSADeletePermission(accountId)]}>
<IconButton icon={<Trash />} onClick={handleDelete} />
</AuthZTooltip>
```
### withAuthZPage (preferred for pages)
HOC for route-level gating. Wrap at export. Shows `PermissionDeniedFullPage` + `AppLoading`.
```tsx
import { RoleListPermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
function RolesPage(): JSX.Element {
return <div>...</div>;
}
export default withAuthZPage(RolesPage, {
checks: [RoleListPermission],
});
```
### withAuthZContent (preferred for sections)
HOC for inline sections. Shows `PermissionDeniedCallout` on deny.
```tsx
import { buildRoleReadPermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
function RoleEditor(): JSX.Element {
return <div>...</div>;
}
// Dynamic checks from route params
export default withAuthZContent(RoleEditor, {
checks: (_props, ctx) => [buildRoleReadPermission(ctx.params.roleId)],
});
```
### withAuthZ
HOC base. No default fallback. Use when you need custom fallback.
```tsx
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
export default withAuthZ(SecretPanel, {
checks: [buildPermission('write', 'settings:org')],
fallback: <p>No access</p>,
});
```
### AuthZGuardPage
JSX variant of `withAuthZPage`. Use when HOC not possible (conditional rendering).
```tsx
import { RoleListPermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
<AuthZGuardPage checks={[RoleListPermission]}>
<RolesPage />
</AuthZGuardPage>
```
### AuthZGuardContent
JSX variant of `withAuthZContent`. Use when HOC not possible.
```tsx
import { RoleCreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
<AuthZGuardContent checks={[RoleCreatePermission]}>
<RoleEditor />
</AuthZGuardContent>
```
### AuthZGuard
JSX base guard. No default fallback. Use when you need custom fallback in JSX.
```tsx
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
<AuthZGuard
checks={[buildPermission('write', 'settings:org')]}
fallback={<p>No access</p>}
fallbackOnLoading={<Spinner />}
>
<SecretContent />
</AuthZGuard>
```
## Fallback Components
Don't use these components directly, always prefer using via `withAuthZ` and their variants.
- PermissionDeniedCallout: inline error callout. Shows `user/{id} is not authorized to perform {permissions}`.
- PermissionDeniedFullPage: full-page centered error. Same message, bigger presentation.

View File

@@ -0,0 +1,440 @@
import { ReactElement } from 'react';
import type { RouteComponentProps } from 'react-router-dom';
import type {
AuthtypesGettableTransactionDTO,
AuthtypesTransactionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, waitFor } from 'tests/test-utils';
import {
AUTHZ_CHECK_URL,
authzMockResponse,
} from 'lib/authz/utils/authz-test-utils';
import { createGuardedRoute } from './createGuardedRoute';
describe('createGuardedRoute', () => {
const TestComponent = ({ testProp }: { testProp: string }): ReactElement => (
<div>Test Component: {testProp}</div>
);
it('should render component when permission is granted', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(TestComponent, 'read', 'role:*');
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should substitute route parameters in object string', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'role:{id}',
);
const mockMatch = {
params: { id: '123' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/123',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should handle multiple route parameters', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = (await req.json()) as AuthtypesTransactionDTO[];
const txn = payload[0];
const responseData: AuthtypesGettableTransactionDTO[] = [
{
relation: txn.relation,
object: {
resource: {
kind: txn.object.resource.kind,
type: txn.object.resource.type,
},
selector: '123:456',
},
authorized: true,
},
];
return res(
ctx.status(200),
ctx.json({ data: responseData, status: 'success' }),
);
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'update',
'role:{id}:{version}',
);
const mockMatch = {
params: { id: '123', version: '456' },
isExact: true,
path: '/dashboard/:id/:version',
url: '/dashboard/123/456',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should keep placeholder when route parameter is missing', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'role:{id}',
);
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should render loading fallback when loading', () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (_req, res, ctx) => {
return res(
ctx.delay('infinite'),
ctx.status(200),
ctx.json({ data: [], status: 'success' }),
);
}),
);
const GuardedComponent = createGuardedRoute(TestComponent, 'read', 'role:*');
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
expect(screen.getByText('SigNoz')).toBeInTheDocument();
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();
});
it('should render the component when API error occurs (fail open)', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
const GuardedComponent = createGuardedRoute(TestComponent, 'read', 'role:*');
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should render no permissions fallback when permission is denied', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'update',
'role:{id}',
);
const mockMatch = {
params: { id: '123' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/123',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
const heading = document.querySelector('h3');
expect(heading).toBeInTheDocument();
expect(heading?.textContent).toMatch(/not authorized/i);
});
expect(screen.getByText(/update:role:123/)).toBeInTheDocument();
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();
});
it('should pass all props to wrapped component', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const ComponentWithMultipleProps = ({
prop1,
prop2,
prop3,
}: {
prop1: string;
prop2: number;
prop3: boolean;
}): ReactElement => (
<div>
{prop1} - {prop2} - {prop3.toString()}
</div>
);
const GuardedComponent = createGuardedRoute(
ComponentWithMultipleProps,
'read',
'role:*',
);
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
prop1: 'value1',
prop2: 42,
prop3: true,
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('value1 - 42 - true')).toBeInTheDocument();
});
});
it('should memoize resolved object based on route params', async () => {
let requestCount = 0;
const requestedObjects: string[] = [];
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
requestCount++;
const payload = (await req.json()) as AuthtypesTransactionDTO[];
const obj = payload[0]?.object;
const kind = obj?.resource?.kind;
const selector = obj?.selector ?? '*';
const objectStr = `${kind}:${selector}`;
requestedObjects.push(objectStr ?? '');
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'role:{id}',
);
const mockMatch1 = {
params: { id: '123' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/123',
};
const props1 = {
testProp: 'test-value-1',
match: mockMatch1,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
const { unmount } = render(<GuardedComponent {...props1} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value-1')).toBeInTheDocument();
});
expect(requestCount).toBe(1);
expect(requestedObjects).toContain('role:123');
unmount();
const mockMatch2 = {
params: { id: '456' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/456',
};
const props2 = {
testProp: 'test-value-2',
match: mockMatch2,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props2} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value-2')).toBeInTheDocument();
});
expect(requestCount).toBe(2);
expect(requestedObjects).toContain('role:456');
});
it('should handle different relation types', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'delete',
'role:{id}',
);
const mockMatch = {
params: { id: '789' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/789',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,41 @@
.guard-authz-error-no-authz {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 24px;
.guard-authz-error-no-authz-content {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 8px;
max-width: 500px;
}
img {
width: 32px;
height: 32px;
}
h3 {
font-size: 18px;
color: var(--l1-foreground);
line-height: 18px;
}
p {
font-size: 14px;
color: var(--l3-foreground);
line-height: 18px;
span {
background-color: var(--l3-background);
white-space: nowrap;
padding: 0 2px;
}
}
}

View File

@@ -0,0 +1,67 @@
import { ComponentType, ReactElement, useMemo } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import {
AuthZObject,
AuthZRelation,
BrandedPermission,
} from 'lib/authz/hooks/useAuthZ/types';
import { formatPermission } from 'lib/authz/hooks/useAuthZ/utils';
import { useAppContext } from 'providers/App/App';
import noDataUrl from 'assets/Icons/no-data.svg';
import AppLoading from '../../../../components/AppLoading/AppLoading';
import { GuardAuthZ } from '../GuardAuthZ/GuardAuthZ';
import './createGuardedRoute.styles.scss';
function OnNoPermissionsFallback(response: {
requiredPermissionName: BrandedPermission;
}): ReactElement {
const { user } = useAppContext();
return (
<div className="guard-authz-error-no-authz">
<div className="guard-authz-error-no-authz-content">
<img src={noDataUrl} alt="No permission" />
<h3>Uh-oh! You are not authorized</h3>
<p>
<code>user/{user.id}</code> is not authorized to perform{' '}
<code>{formatPermission(response.requiredPermissionName)}</code>
</p>
</div>
</div>
);
}
// eslint-disable-next-line @typescript-eslint/ban-types
export function createGuardedRoute<P extends object, R extends AuthZRelation>(
Component: ComponentType<P>,
relation: R,
object: AuthZObject<R>,
): ComponentType<P & RouteComponentProps<Record<string, string>>> {
return function GuardedRouteComponent(
props: P & RouteComponentProps<Record<string, string>>,
): ReactElement {
const resolvedObject = useMemo(() => {
const paramPattern = /\{([^}]+)\}/g;
return object.replace(paramPattern, (match, paramName) => {
const paramValue = props.match?.params?.[paramName];
return paramValue !== undefined ? paramValue : match;
}) as AuthZObject<R>;
}, [props.match?.params]);
return (
<GuardAuthZ
relation={relation}
object={resolvedObject}
fallbackOnLoading={<AppLoading />}
fallbackOnNoPermissions={(response): ReactElement => (
<OnNoPermissionsFallback {...response} />
)}
>
<Component {...props} />
</GuardAuthZ>
);
};
}

View File

@@ -1,578 +0,0 @@
import React from 'react';
import { render, screen, waitFor, act } from 'tests/test-utils';
import { useQueryClient } from 'react-query';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import {
AUTHZ_CHECK_URL,
authzMockResponse,
setupAuthzAllow,
setupAuthzDeny,
setupAuthzGrantByPrefix,
} from 'lib/authz/utils/authz-test-utils';
import type { AuthZObject } from 'lib/authz/hooks/useAuthZ/types';
import {
buildObjectString,
buildPermission,
} from 'lib/authz/hooks/useAuthZ/utils';
import { withAuthZ, RouterContext } from './withAuthZ';
import { withAuthZContent } from './withAuthZContent';
import { withAuthZPage } from './withAuthZPage';
const mockUseParams = jest.fn();
const mockUseLocation = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: (): Record<string, string> => mockUseParams(),
useLocation: (): { pathname: string; search: string } => mockUseLocation(),
}));
const readPerm = buildPermission('read', 'role:*' as AuthZObject<'read'>);
function Base(): JSX.Element {
return <div>Base component</div>;
}
beforeEach(() => {
mockUseParams.mockReturnValue({});
mockUseLocation.mockReturnValue({ pathname: '/', search: '' });
});
describe('withAuthZ', () => {
it('renders the wrapped component when allowed', async () => {
server.use(setupAuthzAllow(readPerm));
const Guarded = withAuthZ(Base, { checks: [readPerm] });
render(<Guarded />);
await waitFor(() => {
expect(screen.getByText('Base component')).toBeInTheDocument();
});
});
it('renders nothing when denied without a fallback', async () => {
server.use(setupAuthzDeny(readPerm));
const Guarded = withAuthZ(Base, { checks: [readPerm] });
render(<Guarded />);
await waitFor(() => {
expect(screen.queryByText('Base component')).not.toBeInTheDocument();
});
});
it('renders the provided fallback when denied', async () => {
server.use(setupAuthzDeny(readPerm));
const Guarded = withAuthZ(Base, {
checks: [readPerm],
fallback: <div>No access</div>,
});
render(<Guarded />);
await waitFor(() => {
expect(screen.getByText('No access')).toBeInTheDocument();
});
});
it('resolves checks from props via the selector form', async () => {
type Props = { roleId: string };
const RoleView = ({ roleId }: Props): JSX.Element => <div>role {roleId}</div>;
const deniedPerm = buildPermission(
'read',
buildObjectString<'read'>('role', 'r-1'),
);
server.use(setupAuthzDeny(deniedPerm));
const Guarded = withAuthZ<Props>(RoleView, {
checks: ({ roleId }) => [
buildPermission('read', buildObjectString<'read'>('role', roleId)),
],
fallback: <div>denied selector</div>,
});
render(<Guarded roleId="r-1" />);
await waitFor(() => {
expect(screen.getByText('denied selector')).toBeInTheDocument();
});
expect(screen.queryByText('role r-1')).not.toBeInTheDocument();
});
it('sets a descriptive displayName', () => {
const Guarded = withAuthZ(Base, { checks: [readPerm] });
expect(Guarded.displayName).toBe('withAuthZ(Base)');
});
});
describe('withAuthZPage', () => {
it('renders the full-page denied screen when denied', async () => {
server.use(setupAuthzDeny(readPerm));
const Guarded = withAuthZPage(Base, { checks: [readPerm] });
render(<Guarded />);
await waitFor(() => {
expect(
screen.getByText('Uh-oh! You are not authorized'),
).toBeInTheDocument();
});
});
});
describe('withAuthZContent', () => {
it('renders the denied callout when denied', async () => {
server.use(setupAuthzDeny(readPerm));
const Guarded = withAuthZContent(Base, { checks: [readPerm] });
render(<Guarded />);
await waitFor(() => {
expect(screen.getByText('read:role:*')).toBeInTheDocument();
});
expect(screen.queryByText('Base component')).not.toBeInTheDocument();
});
});
describe('withAuthZ router context', () => {
it('extracts checks from route params via router.params', async () => {
mockUseParams.mockReturnValue({ roleId: 'r-123' });
const rolePerm = buildPermission(
'read',
buildObjectString<'read'>('role', 'r-123'),
);
server.use(setupAuthzAllow(rolePerm));
const RoleView = (): JSX.Element => <div>role view</div>;
const Guarded = withAuthZ(RoleView, {
checks: (_props, router: RouterContext) => [
buildPermission(
'read',
buildObjectString<'read'>('role', router.params.roleId ?? ''),
),
],
});
render(<Guarded />);
await waitFor(() => {
expect(screen.getByText('role view')).toBeInTheDocument();
});
});
it('extracts checks from query params via router.searchParams', async () => {
mockUseLocation.mockReturnValue({
pathname: '/roles',
search: '?roleId=r-456',
});
const rolePerm = buildPermission(
'read',
buildObjectString<'read'>('role', 'r-456'),
);
server.use(setupAuthzAllow(rolePerm));
const RoleListView = (): JSX.Element => <div>role list view</div>;
const Guarded = withAuthZ(RoleListView, {
checks: (_props, router: RouterContext) => [
buildPermission(
'read',
buildObjectString<'read'>('role', router.searchParams.get('roleId') ?? ''),
),
],
});
render(<Guarded />);
await waitFor(() => {
expect(screen.getByText('role list view')).toBeInTheDocument();
});
});
it('extracts checks from pathname via router.matchPath', async () => {
mockUseLocation.mockReturnValue({
pathname: '/settings/roles/r-789/edit',
search: '',
});
const rolePerm = buildPermission(
'update',
buildObjectString<'update'>('role', 'r-789'),
);
server.use(setupAuthzAllow(rolePerm));
const EditRoleView = (): JSX.Element => <div>edit role</div>;
const Guarded = withAuthZ(EditRoleView, {
checks: (_props, router: RouterContext) => {
const match = router.matchPath<{ roleId: string }>(
'/settings/roles/:roleId/edit',
);
return match
? [
buildPermission(
'update',
buildObjectString<'update'>('role', match.roleId),
),
]
: [];
},
});
render(<Guarded />);
await waitFor(() => {
expect(screen.getByText('edit role')).toBeInTheDocument();
});
});
it('denies when router-derived permission is not allowed', async () => {
mockUseParams.mockReturnValue({ roleId: 'r-denied' });
const deniedPerm = buildPermission(
'read',
buildObjectString<'read'>('role', 'r-denied'),
);
server.use(setupAuthzDeny(deniedPerm));
const RoleView = (): JSX.Element => <div>role view</div>;
const Guarded = withAuthZ(RoleView, {
checks: (_props, router: RouterContext) => [
buildPermission(
'read',
buildObjectString<'read'>('role', router.params.roleId ?? ''),
),
],
fallback: <div>access denied</div>,
});
render(<Guarded />);
await waitFor(() => {
expect(screen.getByText('access denied')).toBeInTheDocument();
});
expect(screen.queryByText('role view')).not.toBeInTheDocument();
});
});
describe('withAuthZ router context stability', () => {
let renderCount = 0;
function RenderCounter(): JSX.Element {
renderCount += 1;
return <div data-testid="render-count">{renderCount}</div>;
}
beforeEach(() => {
renderCount = 0;
server.use(setupAuthzGrantByPrefix('read||__||role'));
});
it('does not re-render when useParams returns new object with same values', async () => {
mockUseParams.mockReturnValue({ roleId: 'r-1' });
mockUseLocation.mockReturnValue({ pathname: '/', search: '' });
const Guarded = withAuthZ(RenderCounter, {
checks: (_props, router: RouterContext) => [
buildPermission(
'read',
buildObjectString<'read'>('role', router.params.roleId ?? '*'),
),
],
});
const { rerender } = render(<Guarded />);
await waitFor(() => {
expect(screen.getByTestId('render-count')).toBeInTheDocument();
});
const initialCount = renderCount;
// Return NEW object with SAME values — should not cause re-render
mockUseParams.mockReturnValue({ roleId: 'r-1' });
rerender(<Guarded />);
// Allow any pending effects to flush
await waitFor(() => {
expect(renderCount).toBe(initialCount);
});
});
it('does not re-render when useLocation returns new object with same pathname', async () => {
mockUseParams.mockReturnValue({});
mockUseLocation.mockReturnValue({ pathname: '/roles', search: '' });
const Guarded = withAuthZ(RenderCounter, {
checks: (_props, router: RouterContext) => {
const match = router.matchPath<{ id: string }>('/roles/:id');
return match
? [buildPermission('read', buildObjectString<'read'>('role', match.id))]
: [readPerm];
},
});
const { rerender } = render(<Guarded />);
await waitFor(() => {
expect(screen.getByTestId('render-count')).toBeInTheDocument();
});
const initialCount = renderCount;
// Return NEW object with SAME pathname — should not cause re-render
mockUseLocation.mockReturnValue({ pathname: '/roles', search: '' });
rerender(<Guarded />);
await waitFor(() => {
expect(renderCount).toBe(initialCount);
});
});
it('does not re-render when useLocation returns new object with same search params', async () => {
mockUseParams.mockReturnValue({});
mockUseLocation.mockReturnValue({ pathname: '/', search: '?tab=keys' });
const Guarded = withAuthZ(RenderCounter, {
checks: (_props, router: RouterContext) => {
// Access searchParams to ensure it's part of the dependency chain
void router.searchParams.get('tab');
return [readPerm];
},
});
const { rerender } = render(<Guarded />);
await waitFor(() => {
expect(screen.getByTestId('render-count')).toBeInTheDocument();
});
const initialCount = renderCount;
// Return NEW object with SAME search — should not cause re-render
mockUseLocation.mockReturnValue({ pathname: '/', search: '?tab=keys' });
rerender(<Guarded />);
await waitFor(() => {
expect(renderCount).toBe(initialCount);
});
});
it('re-renders when params values actually change', async () => {
mockUseParams.mockReturnValue({ roleId: 'r-1' });
mockUseLocation.mockReturnValue({ pathname: '/', search: '' });
const Guarded = withAuthZ(RenderCounter, {
checks: (_props, router: RouterContext) => [
buildPermission(
'read',
buildObjectString<'read'>('role', router.params.roleId ?? '*'),
),
],
});
const { unmount } = render(<Guarded />);
await waitFor(() => {
expect(screen.getByTestId('render-count')).toBeInTheDocument();
});
const initialCount = renderCount;
unmount();
// Return DIFFERENT values — re-mount with new mock values
mockUseParams.mockReturnValue({ roleId: 'r-2' });
render(<Guarded />);
await waitFor(() => {
expect(renderCount).toBeGreaterThan(initialCount);
});
});
it('re-renders when pathname actually changes', async () => {
mockUseParams.mockReturnValue({});
mockUseLocation.mockReturnValue({ pathname: '/roles', search: '' });
const Guarded = withAuthZ(RenderCounter, {
checks: [readPerm],
});
const { unmount } = render(<Guarded />);
await waitFor(() => {
expect(screen.getByTestId('render-count')).toBeInTheDocument();
});
const initialCount = renderCount;
unmount();
// DIFFERENT pathname — re-mount with new mock values
mockUseLocation.mockReturnValue({ pathname: '/users', search: '' });
render(<Guarded />);
await waitFor(() => {
expect(renderCount).toBeGreaterThan(initialCount);
});
});
it('re-renders when search params actually change', async () => {
mockUseParams.mockReturnValue({});
mockUseLocation.mockReturnValue({ pathname: '/', search: '?tab=keys' });
const Guarded = withAuthZ(RenderCounter, {
checks: [readPerm],
});
const { unmount } = render(<Guarded />);
await waitFor(() => {
expect(screen.getByTestId('render-count')).toBeInTheDocument();
});
const initialCount = renderCount;
unmount();
// DIFFERENT search — re-mount with new mock values
mockUseLocation.mockReturnValue({ pathname: '/', search: '?tab=details' });
render(<Guarded />);
await waitFor(() => {
expect(renderCount).toBeGreaterThan(initialCount);
});
});
});
describe('withAuthZContent cache invalidation', () => {
const testPerm = buildPermission(
'read',
'role:test-invalidation' as AuthZObject<'read'>,
);
// Callout displays permission as "relation:object" format
const displayedPerm = 'read:role:test-invalidation';
function ContentComponent(): JSX.Element {
return <div data-testid="protected-content">Protected Content</div>;
}
function InvalidationTrigger({
permission,
onReady,
}: {
permission: string;
onReady: (invalidate: () => Promise<void>) => void;
}): null {
const queryClient = useQueryClient();
React.useEffect(() => {
onReady(async () => {
// Reset query to initial state and trigger refetch (matches devtools behavior)
await queryClient.resetQueries(['authz', permission]);
});
}, [queryClient, permission, onReady]);
return null;
}
it('re-renders from allowed to denied when cache is invalidated', async () => {
let shouldGrant = true;
let invalidateFn: (() => Promise<void>) | null = null;
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(
ctx.status(200),
ctx.json(authzMockResponse(payload, [shouldGrant])),
);
}),
);
const Guarded = withAuthZContent(ContentComponent, { checks: [testPerm] });
render(
<>
<Guarded />
<InvalidationTrigger
permission={testPerm}
onReady={(fn): void => {
invalidateFn = fn;
}}
/>
</>,
);
// Initially allowed - should show content
await waitFor(() => {
expect(screen.getByTestId('protected-content')).toBeInTheDocument();
});
// Change server response to deny
shouldGrant = false;
// Invalidate cache
await act(async () => {
await invalidateFn?.();
});
// Should now show denied callout
await waitFor(() => {
expect(screen.queryByTestId('protected-content')).not.toBeInTheDocument();
});
// Callout should show the denied permission
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByText(displayedPerm)).toBeInTheDocument();
});
it('re-renders from denied to allowed when cache is invalidated', async () => {
let shouldGrant = false;
let invalidateFn: (() => Promise<void>) | null = null;
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(
ctx.status(200),
ctx.json(authzMockResponse(payload, [shouldGrant])),
);
}),
);
const Guarded = withAuthZContent(ContentComponent, { checks: [testPerm] });
render(
<>
<Guarded />
<InvalidationTrigger
permission={testPerm}
onReady={(fn): void => {
invalidateFn = fn;
}}
/>
</>,
);
// Initially denied - should show callout
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
});
expect(screen.queryByTestId('protected-content')).not.toBeInTheDocument();
// Change server response to allow
shouldGrant = true;
// Invalidate cache
await act(async () => {
await invalidateFn?.();
});
// Should now show protected content
await waitFor(() => {
expect(screen.getByTestId('protected-content')).toBeInTheDocument();
});
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
});

View File

@@ -1,13 +0,0 @@
import { ComponentType } from 'react';
import { AuthZGuard } from 'lib/authz/components/AuthZGuard/AuthZGuard';
import { createAuthZHOC, WithAuthZOptions } from './withAuthZ.utils';
export type { RouterContext, WithAuthZOptions } from './withAuthZ.utils';
export function withAuthZ<P extends object>(
Component: ComponentType<P>,
opts: WithAuthZOptions<P>,
): ComponentType<P> {
return createAuthZHOC(AuthZGuard, 'withAuthZ', Component, opts);
}

View File

@@ -1,108 +0,0 @@
import { ComponentType, ReactElement, createElement, useMemo } from 'react';
import {
matchPath as reactRouterMatchPath,
useLocation,
useParams,
} from 'react-router-dom';
import type { AuthZGuardProps } from 'lib/authz/components/AuthZGuard/AuthZGuard';
import type { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
export type RouterContext = {
/**
* Route params from useParams (e.g. `/roles/:roleId` → `{ roleId: "r-1" }`)
*/
params: Record<string, string | undefined>;
pathname: string;
/**
* Query params as URLSearchParams (use `.get('key')` to read)
*/
searchParams: URLSearchParams;
/**
* Extract params from pathname using a route pattern.
* Returns null if pattern doesn't match.
* @example router.matchPath<{ id: string }>('/edit/:id')?.id
*/
matchPath: <Params extends Record<string, string>>(
pattern: string,
) => Params | null;
};
export type WithAuthZOptions<P> = {
/**
* Static checks, or a selector deriving them from props and router context.
* Use router context to extract dynamic values from route params, pathname, or query params.
* @example
* // From route params
* checks: (props, router) => [buildPermission('read', `role:${router.params.roleId}`)]
* // From query params
* checks: (props, router) => [buildPermission('read', `dashboard:${router.searchParams.get('id')}`)]
* // From pathname matching
* checks: (props, router) => {
* const match = router.matchPath<{ id: string }>('/edit/:id');
* return match ? [buildPermission('update', `role:${match.id}`)] : [];
* }
*/
checks:
| BrandedPermission[]
| ((props: P, router: RouterContext) => BrandedPermission[]);
fallback?: AuthZGuardProps['fallback'];
fallbackOnLoading?: AuthZGuardProps['fallbackOnLoading'];
failOpenOnError?: AuthZGuardProps['onFailRenderContent'];
};
function useStableParams(): Record<string, string | undefined> {
const params = useParams();
const paramsJson = JSON.stringify(params);
return useMemo(() => JSON.parse(paramsJson), [paramsJson]);
}
function useRouterContext(): RouterContext {
const params = useStableParams();
const { pathname, search } = useLocation();
const searchParams = useMemo(() => new URLSearchParams(search), [search]);
return useMemo(
(): RouterContext => ({
params,
pathname,
searchParams,
matchPath: <Params extends Record<string, string>>(
pattern: string,
): Params | null => {
const match = reactRouterMatchPath<Params>(pathname, {
path: pattern,
exact: false,
});
return match?.params ?? null;
},
}),
[params, pathname, searchParams],
);
}
export function createAuthZHOC<P extends object>(
Guard: ComponentType<AuthZGuardProps>,
hocName: string,
Component: ComponentType<P>,
opts: WithAuthZOptions<P>,
): ComponentType<P> {
const { checks, ...guardProps } = opts;
function Wrapped(props: P): ReactElement | null {
const router = useRouterContext();
const resolvedChecks =
typeof checks === 'function' ? checks(props, router) : checks;
return (
<Guard checks={resolvedChecks} {...guardProps}>
{createElement(Component, props)}
</Guard>
);
}
Wrapped.displayName = `${hocName}(${
Component.displayName || Component.name || 'Component'
})`;
return Wrapped;
}

View File

@@ -1,11 +0,0 @@
import { ComponentType } from 'react';
import { AuthZGuardContent } from 'lib/authz/components/AuthZGuard/AuthZGuardContent';
import { createAuthZHOC, WithAuthZOptions } from './withAuthZ.utils';
export function withAuthZContent<P extends object>(
Component: ComponentType<P>,
opts: WithAuthZOptions<P>,
): ComponentType<P> {
return createAuthZHOC(AuthZGuardContent, 'withAuthZContent', Component, opts);
}

View File

@@ -1,11 +0,0 @@
import { ComponentType } from 'react';
import { AuthZGuardPage } from 'lib/authz/components/AuthZGuard/AuthZGuardPage';
import { createAuthZHOC, WithAuthZOptions } from './withAuthZ.utils';
export function withAuthZPage<P extends object>(
Component: ComponentType<P>,
opts: WithAuthZOptions<P>,
): ComponentType<P> {
return createAuthZHOC(AuthZGuardPage, 'withAuthZPage', Component, opts);
}

View File

@@ -1,125 +0,0 @@
# AuthZ Test Utilities
Helpers for testing permission-gated components.
## File Naming
AuthZ tests live in `*.authz.test.tsx` files alongside other test files:
```
ComponentName/
├── ComponentName.tsx
├── __tests__/
│ ├── ComponentName.test.tsx # functional tests
│ └── ComponentName.authz.test.tsx # permission tests
```
## Test Structure
```tsx
import { server } from 'mocks-server/server';
import { setupAuthzAdmin, setupAuthzDenyAll } from 'lib/authz/utils/authz-test-utils';
import { render, screen, waitFor } from 'tests/test-utils';
describe('ComponentName - AuthZ', () => {
afterEach(() => {
jest.restoreAllMocks();
server.resetHandlers(); // reset MSW handlers after each test
});
describe('permission denied', () => {
it('shows permission denied when read denied', async () => {
server.use(setupAuthzDenyAll());
render(<ComponentName />);
await expect(
screen.findByText(/not authorized/i),
).resolves.toBeInTheDocument();
});
});
describe('permission granted', () => {
it('renders content when permitted', async () => {
server.use(setupAuthzAdmin());
render(<ComponentName />);
await waitFor(() => {
expect(screen.getByTestId('protected-content')).toBeInTheDocument();
});
});
});
});
```
Key points:
- Use `server.use()` at start of each test (not `beforeEach`) for explicit setup
- Call `server.resetHandlers()` in `afterEach` to avoid test pollution
- Use `waitFor` or `findBy*` queries since authz checks are async
- Group tests by permission scenario: denied, granted, partial, loading
## MSW Handlers
Mock `/api/v1/authz/check` endpoint responses.
```tsx
import { server } from 'mocks-server/server';
import {
setupAuthzAdmin,
setupAuthzDenyAll,
setupAuthzDeny,
setupAuthzAllow,
setupAuthzGrantByPrefix,
} from 'lib/authz/utils/authz-test-utils';
// Grant all permissions
server.use(setupAuthzAdmin());
// Deny all permissions
server.use(setupAuthzDenyAll());
// Grant all except specific permissions
server.use(setupAuthzDeny(RoleCreatePermission, RoleDeletePermission));
// Deny all except specific permissions
server.use(setupAuthzAllow(RoleListPermission));
// Grant by relation prefix (e.g., grant read/delete, deny update)
server.use(setupAuthzGrantByPrefix('read', 'delete'));
```
## Custom Mock Response
For fine-grained control over responses.
```tsx
import { rest } from 'msw';
import { AUTHZ_CHECK_URL, authzMockResponse } from 'lib/authz/utils/authz-test-utils';
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
// [true, false] = first permission granted, second denied
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true, false])));
}),
);
```
## Testing Loading State
Use `ctx.delay('infinite')` to hold response indefinitely:
```tsx
it('shows skeleton while checking permissions', () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) =>
res(ctx.delay('infinite')),
),
);
render(<ComponentName />);
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
});
```

View File

@@ -104,25 +104,6 @@ export function setupAuthzAllow(
});
}
/** Grants permissions that start with any of the given prefixes. */
export function setupAuthzGrantByPrefix(...prefixes: string[]): RestHandler {
return rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = (await req.json()) as AuthtypesTransactionDTO[];
return res(
ctx.status(200),
ctx.json(
authzMockResponse(
payload,
payload.map((txn) => {
const perm = gettableTransactionToPermission(txn);
return prefixes.some((prefix) => perm.startsWith(prefix));
}),
),
),
);
});
}
export function buildLicense(
overrides?: Partial<LicenseResModel>,
): LicenseResModel {

View File

@@ -26,8 +26,8 @@ export default function AIAssistantPage(): JSX.Element {
// Skip the mount-time Opened fire when the user expanded an already-open
// drawer/modal — that surface already emitted Opened with the right source.
// Router state (vs a module flag) survives page remounts and aborted
// navigations.
// Router state (vs a module flag) survives StrictMode double-mount and
// aborted navigations.
const fromInApp = location.state?.fromInApp === true;
useEffect(() => {
if (fromInApp) {
@@ -52,34 +52,18 @@ export default function AIAssistantPage(): JSX.Element {
(s) => s.startNewConversation,
);
// Keep refs so the effect can read the latest store state without re-firing
// when it mutates the store mid-effect (it only depends on the URL param).
// Keep a ref so the effect can read latest conversations without re-firing
// when startNewConversation mutates the store mid-effect.
const conversationsRef = useRef(conversations);
conversationsRef.current = conversations;
const activeConversationIdRef = useRef(activeConversationId);
activeConversationIdRef.current = activeConversationId;
useEffect(() => {
// URL points at a known conversation → just activate it.
if (conversationId && conversationsRef.current[conversationId]) {
if (conversationsRef.current[conversationId]) {
setActiveConversation(conversationId);
return;
} else {
const newId = startNewConversation();
history.replace(ROUTES.AI_ASSISTANT.replace(':conversationId', newId));
}
// The URL has no usable conversation id (bare `/ai-assistant`, or a stale
// param). Prefer resuming the active conversation — including the
// rehydrating placeholder for the persisted thread — over minting a new
// one. This is what stops a throwaway blank chat from flashing as a
// second thread during load, and stops a duplicate when the page
// remounts during startup route churn (the active id is already set, so
// we resume instead of create). Starting fresh is the last resort, only
// when there is genuinely nothing to resume.
const activeId = activeConversationIdRef.current;
const resumeId =
activeId && conversationsRef.current[activeId]
? activeId
: startNewConversation();
history.replace(ROUTES.AI_ASSISTANT.replace(':conversationId', resumeId));
// Only re-run when the URL param changes, not when conversations mutates.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [conversationId]);

View File

@@ -1,181 +0,0 @@
import { MemoryRouter, Route } from 'react-router-dom';
// eslint-disable-next-line no-restricted-imports
import { render } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('container/AIAssistant/ConversationView', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="conversation-view" />,
}));
jest.mock('container/AIAssistant/components/ConversationsList', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="conversations-list" />,
}));
jest.mock('components/Noz/Noz', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="noz" />,
}));
jest.mock('container/AIAssistant/hooks/useAIAssistantAnalyticsContext', () => ({
normalizePage: (page: string): string => page,
useAIAssistantAnalyticsContext: (): unknown => ({ mode: 'page' }),
}));
// eslint-disable-next-line import/first
import AIAssistantPage from '../AIAssistantPage';
function renderAt(entry: string): { unmount: () => void } {
return render(
<MemoryRouter initialEntries={[entry]}>
<Route
exact
path={[ROUTES.AI_ASSISTANT_BASE, ROUTES.AI_ASSISTANT]}
component={AIAssistantPage}
/>
</MemoryRouter>,
);
}
function renderAtBase(): { unmount: () => void } {
return renderAt(ROUTES.AI_ASSISTANT_BASE);
}
function conversationCount(): number {
return Object.keys(useAIAssistantStore.getState().conversations).length;
}
function conversationIds(): string[] {
return Object.keys(useAIAssistantStore.getState().conversations);
}
function activeId(): string | null {
return useAIAssistantStore.getState().activeConversationId;
}
describe('AIAssistantPage', () => {
beforeEach(() => {
useAIAssistantStore.setState({
conversations: {},
streams: {},
activeConversationId: null,
});
});
it('opens exactly one conversation when navigating to /ai-assistant', () => {
const { unmount } = renderAtBase();
expect(conversationCount()).toBe(1);
unmount();
});
it('does not stack a second conversation when the page remounts at the bare URL (route churn)', () => {
// First mount at `/ai-assistant` creates one blank conversation and
// redirects to `/ai-assistant/:id`.
const { unmount } = renderAtBase();
expect(conversationCount()).toBe(1);
const firstId = conversationIds()[0];
// Startup route-list churn unmounts and remounts the page while the URL
// is momentarily back at the bare `/ai-assistant`. This previously
// created a second blank conversation — now it reuses the first.
unmount();
const { unmount: unmount2 } = renderAtBase();
expect(conversationCount()).toBe(1);
// The surviving conversation is the original one, resumed — not a fresh mint.
expect(conversationIds()).toStrictEqual([firstId]);
expect(activeId()).toBe(firstId);
unmount2();
});
it('activates the conversation named in the URL without creating a new one', () => {
useAIAssistantStore.setState({
conversations: {
existing: {
id: 'existing',
messages: [],
createdAt: 1,
updatedAt: 1,
},
},
streams: {},
activeConversationId: null,
});
const { unmount } = renderAt(
ROUTES.AI_ASSISTANT.replace(':conversationId', 'existing'),
);
expect(conversationCount()).toBe(1);
expect(activeId()).toBe('existing');
unmount();
});
it('resumes the active conversation on /ai-assistant/new instead of minting a new one', () => {
// The sidenav only routes to `/ai-assistant/new` as a fallback, but if an
// active conversation exists the page must resume it rather than spawn a
// throwaway blank thread for the unknown "new" param.
useAIAssistantStore.setState({
conversations: {
active: {
id: 'active',
messages: [],
createdAt: 1,
updatedAt: 1,
},
},
streams: {},
activeConversationId: 'active',
});
const { unmount } = renderAt(
ROUTES.AI_ASSISTANT.replace(':conversationId', 'new'),
);
expect(conversationCount()).toBe(1);
expect(conversationIds()).toStrictEqual(['active']);
expect(activeId()).toBe('active');
unmount();
});
it('resumes the persisted (hydrating) conversation during load instead of creating a second', () => {
// Simulates `onRehydrateStorage` priming the persisted active
// conversation as a hydrating placeholder before `fetchThreads` resolves.
useAIAssistantStore.setState({
conversations: {
persisted: {
id: 'persisted',
messages: [],
createdAt: 1,
updatedAt: 1,
isHydrating: true,
},
},
streams: {},
activeConversationId: 'persisted',
});
const { unmount } = renderAtBase();
// Opening the bare URL must resume the persisted conversation, not mint a
// throwaway blank alongside it (which flashed as a 2nd thread during load).
expect(conversationCount()).toBe(1);
expect(
Object.keys(useAIAssistantStore.getState().conversations),
).toStrictEqual(['persisted']);
unmount();
});
});

View File

@@ -32,22 +32,6 @@
cursor: help;
}
.descriptionTooltip {
display: block;
overflow-wrap: anywhere;
/* Preserve author-entered line breaks in the description. */
white-space: pre-wrap;
a {
color: var(--accent-primary);
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
}
.publicLink {
display: inline-flex;
align-items: center;

View File

@@ -14,7 +14,6 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { isEmpty } from 'lodash-es';
import { linkifyText } from 'utils/linkifyText';
import { openInNewTab } from 'utils/navigation';
import styles from './DashboardInfo.module.scss';
@@ -144,14 +143,7 @@ function DashboardInfo({
)}
{hasDescription && (
<TooltipSimple
side="bottom"
title={
<span className={styles.descriptionTooltip}>
{linkifyText(description)}
</span>
}
>
<TooltipSimple title={description} disableHoverableContent>
<SolidInfoCircle
className={styles.descriptionIcon}
size={14}

View File

@@ -63,8 +63,13 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
const { user } = useAppContext();
const { showErrorModal } = useErrorModal();
const { patchAsync } = useOptimisticPatch();
const { isPickerOpen, openPicker, closePicker, createPanel } =
useCreatePanel();
const {
isPickerOpen,
openPicker,
closePicker,
createPanel,
targetLayoutIndex,
} = useCreatePanel();
const isAuthor =
!!user?.email && !!dashboard.createdBy && dashboard.createdBy === user.email;
@@ -183,6 +188,7 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
open={isPickerOpen}
onClose={closePicker}
onSelect={createPanel}
defaultLayoutIndex={targetLayoutIndex}
/>
</section>
);

View File

@@ -1,22 +1,69 @@
.panelTypeSection {
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
}
.grid {
align-self: stretch;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.typeButton {
.panelTypeCard {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
border: 1px solid var(--l2-border);
background: var(--l2-background);
padding: 12px;
background: var(--bg-ink-400, #0b0c0e);
border: 1px solid var(--l1-border);
gap: 12px;
cursor: pointer;
font: inherit;
border-radius: 4px;
color: var(--l1-foreground);
cursor: pointer;
text-align: left;
transition:
transform 180ms ease,
border-color 180ms ease;
&:hover {
border-color: var(--bg-robin-500);
background-color: var(--l2-background-hover);
border-color: var(--bg-robin-400);
}
&:active {
transform: translateY(2px);
}
}
.panelTypeCardSelected {
border-color: var(--bg-robin-400);
background-color: var(--l2-background-hover);
box-shadow: inset 0 0 0 1px var(--bg-robin-400);
}
.footerActions {
display: flex;
align-items: flex-end;
justify-content: flex-end;
gap: 8px;
width: 100%;
}
.footerPicker {
// Take all the width left over by the (natural-width) confirm button.
display: flex;
flex: 1;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.pickerLabel {
color: var(--l3-foreground);
font-size: 11px;
font-weight: 500;
letter-spacing: 0.06em;
text-transform: uppercase;
}

View File

@@ -1,45 +1,135 @@
import { Modal } from 'antd';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { DialogWrapper } from '@signozhq/ui/dialog';
import cx from 'classnames';
import { useDashboardSections } from '../../../hooks/useDashboardSections';
import type { PanelKind } from '../../../Panels/types/panelKind';
import { PANEL_TYPES } from './constants';
import SectionPicker from './SectionPicker';
import { buildSectionOptions, resolveDefaultSectionValue } from './utils';
import styles from './PanelTypeSelectionModal.module.scss';
import { Plus } from '@signozhq/icons';
interface PanelTypeSelectionModalProps {
open: boolean;
onClose: () => void;
onSelect: (panelKind: PanelKind) => void;
onSelect: (panelKind: PanelKind, layoutIndex?: number) => void;
/** Section the picker opens on; omit → the untitled root / first section. */
defaultLayoutIndex?: number;
}
/** Fake loader shown on the confirm button before navigating to the editor. */
const CONFIRM_LOADER_MS = 500;
function PanelTypeSelectionModal({
open,
onClose,
onSelect,
defaultLayoutIndex,
}: PanelTypeSelectionModalProps): JSX.Element {
const sections = useDashboardSections();
const options = useMemo(() => buildSectionOptions(sections), [sections]);
const hasSectionPicker = options.length > 1;
const [selectedValue, setSelectedValue] = useState('');
const [selectedPanelKind, setSelectedPanelKind] = useState<PanelKind | null>(
null,
);
const [isSubmitting, setIsSubmitting] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const clearTimer = useCallback((): void => {
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}, []);
// Seed the target section on open; cancel a pending navigation on close.
useEffect(() => {
if (open) {
setSelectedValue(resolveDefaultSectionValue(options, defaultLayoutIndex));
setSelectedPanelKind(null);
setIsSubmitting(false);
} else {
clearTimer();
}
}, [open, options, defaultLayoutIndex, clearTimer]);
useEffect(() => clearTimer, [clearTimer]);
const handleConfirm = (): void => {
if (selectedPanelKind === null || isSubmitting) {
return;
}
setIsSubmitting(true);
const layoutIndex = selectedValue === '' ? undefined : Number(selectedValue);
timerRef.current = setTimeout(() => {
onSelect(selectedPanelKind, layoutIndex);
}, CONFIRM_LOADER_MS);
};
// Footer is always shown; the confirm button is disabled until a panel type is picked.
const footer = (
<div className={styles.footerActions}>
{hasSectionPicker && (
<div className={styles.footerPicker}>
<span className={styles.pickerLabel}>Add panel to</span>
<SectionPicker
options={options}
value={selectedValue}
onChange={setSelectedValue}
/>
</div>
)}
<Button
color="primary"
size="md"
disabled={selectedPanelKind === null}
loading={isSubmitting}
prefix={<Plus size={16} />}
onClick={handleConfirm}
testId="panel-type-confirm"
>
Add Panel
</Button>
</div>
);
return (
<Modal
<DialogWrapper
open={open}
title="Select a panel type"
onCancel={onClose}
footer={null}
destroyOnClose
onOpenChange={(isOpen): void => {
if (!isOpen) {
onClose();
}
}}
title="New Panel"
footer={footer}
>
<div className={styles.grid}>
{PANEL_TYPES.map(({ panelKind, label, Icon }) => (
<Button
key={panelKind}
type="button"
variant="ghost"
className={styles.typeButton}
data-testid={`panel-type-${panelKind}`}
onClick={(): void => onSelect(panelKind)}
>
<Icon size={14} />
{label}
</Button>
))}
<div className={styles.panelTypeSection}>
<span className={styles.pickerLabel}>Select panel type</span>
<div className={styles.grid}>
{PANEL_TYPES.map(({ panelKind, label, Icon }) => (
<button
key={panelKind}
type="button"
className={cx(styles.panelTypeCard, {
[styles.panelTypeCardSelected]: panelKind === selectedPanelKind,
})}
data-testid={`panel-type-${panelKind}`}
aria-pressed={panelKind === selectedPanelKind}
onClick={(): void => setSelectedPanelKind(panelKind)}
>
<Icon size={24} color={Color.BG_ROBIN_400} />
{label}
</button>
))}
</div>
</div>
</Modal>
</DialogWrapper>
);
}

View File

@@ -0,0 +1,55 @@
.select {
width: 100%;
}
.dropdown {
min-width: 260px;
}
.rootIcon {
color: var(--bg-robin-400);
flex-shrink: 0;
}
.sectionIcon {
color: var(--l3-foreground);
flex-shrink: 0;
}
.triggerValue {
display: flex;
align-items: center;
gap: 8px;
overflow: hidden;
}
.triggerLabel {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.optionRow {
display: flex;
align-items: center;
gap: 12px;
padding: 4px 0;
}
.optionText {
display: flex;
flex-direction: column;
gap: 2px;
overflow: hidden;
}
.optionLabel {
color: var(--l1-foreground);
line-height: 1.2;
}
.optionDescription {
color: var(--l3-foreground);
font-size: 12px;
line-height: 1.2;
}

View File

@@ -0,0 +1,65 @@
import { useMemo } from 'react';
// eslint-disable-next-line signoz/no-antd-components
import { Select } from 'antd';
import type { SectionOption } from './types';
import styles from './SectionPicker.module.scss';
interface SectionPickerProps {
options: SectionOption[];
value: string;
onChange: (value: string) => void;
}
function SectionPicker({
options,
value,
onChange,
}: SectionPickerProps): JSX.Element {
// `selectedLabel` (one line) shows in the trigger; `label` (two lines) in the list.
const selectOptions = useMemo(
() =>
options.map((option) => {
const iconClass = option.isRoot ? styles.rootIcon : styles.sectionIcon;
return {
value: option.value,
selectedLabel: (
<span className={styles.triggerValue}>
<option.Icon size={16} className={iconClass} />
<span className={styles.triggerLabel}>{option.label}</span>
</span>
),
label: (
<span
className={styles.optionRow}
data-testid={`panel-section-option-${option.layoutIndex}`}
>
<option.Icon size={16} className={iconClass} />
<span className={styles.optionText}>
<span className={styles.optionLabel}>{option.label}</span>
<span className={styles.optionDescription}>{option.description}</span>
</span>
</span>
),
};
}),
[options],
);
return (
<Select<string>
className={styles.select}
popupClassName={styles.dropdown}
value={value}
onChange={onChange}
data-testid="panel-section-select"
optionLabelProp="selectedLabel"
getPopupContainer={(trigger): HTMLElement =>
trigger.parentElement ?? document.body
}
options={selectOptions}
/>
);
}
export default SectionPicker;

View File

@@ -14,3 +14,16 @@ export interface PanelType {
/** Icon component — the consumer renders it and controls size/color/etc. */
Icon: ComponentType<IconProps>;
}
export interface SectionOption {
/** The section's `layoutIndex`, stringified for the Select value. */
value: string;
layoutIndex: number;
/** Section title, or "Dashboard (root)" for the untitled top-level layout. */
label: string;
/** Caption under the label. */
description: string;
/** Untitled top-level layout (has no section header). */
isRoot: boolean;
Icon: ComponentType<IconProps>;
}

View File

@@ -0,0 +1,41 @@
import { LayoutDashboard, Rows2 } from '@signozhq/icons';
import type { DashboardSection } from '../../../utils';
import type { SectionOption } from './types';
const ROOT_LABEL = 'Dashboard (root)';
const ROOT_DESCRIPTION = 'Top level — no section';
const SECTION_DESCRIPTION = 'Section';
/** Maps dashboard sections to section-picker options (untitled → "root"). */
export function buildSectionOptions(
sections: DashboardSection[],
): SectionOption[] {
return sections.map((section) => {
const isRoot = !section.title;
return {
value: String(section.layoutIndex),
layoutIndex: section.layoutIndex,
label: isRoot ? ROOT_LABEL : (section.title as string),
description: isRoot ? ROOT_DESCRIPTION : SECTION_DESCRIPTION,
isRoot,
Icon: isRoot ? LayoutDashboard : Rows2,
};
});
}
/**
* Picks the option the picker should open on: the section the "Add panel" was
* triggered from when present and still valid, otherwise the first option.
*/
export function resolveDefaultSectionValue(
options: SectionOption[],
defaultLayoutIndex: number | undefined,
): string {
const fallback = options[0]?.value ?? '';
if (defaultLayoutIndex === undefined) {
return fallback;
}
const target = String(defaultLayoutIndex);
return options.some((option) => option.value === target) ? target : fallback;
}

View File

@@ -29,8 +29,13 @@ interface SectionProps {
function Section({ section, sections, dragHandle }: SectionProps): JSX.Element {
const isEditable = useDashboardStore((s) => s.isEditable);
const { isPickerOpen, openPicker, closePicker, createPanel } =
useCreatePanel();
const {
isPickerOpen,
openPicker,
closePicker,
createPanel,
targetLayoutIndex,
} = useCreatePanel();
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Placeholder signal for lazy panel query-loading (consumed in a later PR):
@@ -141,6 +146,7 @@ function Section({ section, sections, dragHandle }: SectionProps): JSX.Element {
open={isPickerOpen}
onClose={closePicker}
onSelect={createPanel}
defaultLayoutIndex={targetLayoutIndex}
/>
<ConfirmDeleteDialog
open={isDeleteOpen}

View File

@@ -12,7 +12,10 @@ interface UseCreatePanelResult {
/** Pass the target section's layout index; omit → last/new section. */
openPicker: (layoutIndex?: number) => void;
closePicker: () => void;
createPanel: (panelKind: PanelKind) => void;
/** The section the picker was opened against — seeds its section dropdown. */
targetLayoutIndex: number | undefined;
/** `layoutIndex` overrides the opened-against target (the dropdown's choice). */
createPanel: (panelKind: PanelKind, layoutIndex?: number) => void;
}
/**
@@ -38,16 +41,23 @@ export function useCreatePanel(): UseCreatePanelResult {
}, []);
const createPanel = useCallback(
(panelKind: PanelKind): void => {
(panelKind: PanelKind, targetIndex?: number): void => {
setIsPickerOpen(false);
const path = generatePath(ROUTES.DASHBOARD_PANEL_EDITOR, {
dashboardId,
panelId: NEW_PANEL_ID,
});
safeNavigate(`${path}${newPanelSearch(panelKind, layoutIndex)}`);
const target = targetIndex ?? layoutIndex;
safeNavigate(`${path}${newPanelSearch(panelKind, target)}`);
},
[safeNavigate, dashboardId, layoutIndex],
);
return { isPickerOpen, openPicker, closePicker, createPanel };
return {
isPickerOpen,
openPicker,
closePicker,
targetLayoutIndex: layoutIndex,
createPanel,
};
}

View File

@@ -0,0 +1,21 @@
import { useMemo } from 'react';
import { useGetDashboardV2 } from 'api/generated/services/dashboard';
import { useDashboardStore } from '../store/useDashboardStore';
import { type DashboardSection, layoutsToSections } from '../utils';
/**
* The current dashboard's sections, read from the already-loaded dashboard
* query. The page fetches via useGetDashboardV2 keyed by id, so this reuses that
* cache (no extra request) instead of prop-drilling the section list.
*/
export function useDashboardSections(): DashboardSection[] {
const dashboardId = useDashboardStore((s) => s.dashboardId);
const { data } = useGetDashboardV2({ id: dashboardId });
const spec = data?.data?.spec;
return useMemo(
() => layoutsToSections(spec?.layouts, spec?.panels),
[spec?.layouts, spec?.panels],
);
}

View File

@@ -1,66 +0,0 @@
import { render, screen } from '@testing-library/react';
import { linkifyText } from '../linkifyText';
describe('linkifyText', () => {
it('returns plain text unchanged when there are no links', () => {
render(<div>{linkifyText('just a plain description')}</div>);
expect(screen.getByText('just a plain description')).toBeInTheDocument();
expect(screen.queryByRole('link')).not.toBeInTheDocument();
});
it('wraps an http(s) URL in an anchor that opens in a new tab', () => {
render(<div>{linkifyText('see https://signoz.io/docs for more')}</div>);
const link = screen.getByRole('link', { name: 'https://signoz.io/docs' });
expect(link).toHaveAttribute('href', 'https://signoz.io/docs');
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
});
it('prefixes bare www. links with https://', () => {
render(<div>{linkifyText('visit www.signoz.io')}</div>);
const link = screen.getByRole('link', { name: 'www.signoz.io' });
expect(link).toHaveAttribute('href', 'https://www.signoz.io');
});
it('keeps trailing punctuation outside the link', () => {
render(<div>{linkifyText('read https://signoz.io.')}</div>);
const link = screen.getByRole('link', { name: 'https://signoz.io' });
expect(link).toHaveAttribute('href', 'https://signoz.io');
});
it('linkifies multiple URLs in the same string', () => {
render(
<div>{linkifyText('a https://one.com and b https://two.com end')}</div>,
);
expect(screen.getAllByRole('link')).toHaveLength(2);
expect(
screen.getByRole('link', { name: 'https://one.com' }),
).toBeInTheDocument();
expect(
screen.getByRole('link', { name: 'https://two.com' }),
).toBeInTheDocument();
});
it('preserves newlines around a link', () => {
const { container } = render(
<div>{linkifyText('line one\nsee https://signoz.io\nline three')}</div>,
);
expect(container.textContent).toBe(
'line one\nsee https://signoz.io\nline three',
);
expect(
screen.getByRole('link', { name: 'https://signoz.io' }),
).toHaveAttribute('href', 'https://signoz.io');
});
it('returns an empty string unchanged', () => {
expect(linkifyText('')).toBe('');
});
});

View File

@@ -1,71 +0,0 @@
import { Fragment, MouseEvent, ReactNode } from 'react';
/** Matches http(s) URLs and bare www. links up to the next whitespace. */
const URL_REGEX = /((?:https?:\/\/|www\.)[^\s]+)/gi;
/** Trailing punctuation that is almost never part of the intended URL. */
const TRAILING_PUNCTUATION = /[.,;:!?)\]}'"]+$/;
const stopPropagation = (
event: MouseEvent<HTMLAnchorElement, globalThis.MouseEvent>,
): void => {
// Prevent parent click listeners (e.g. title edit) from firing.
event.stopPropagation();
};
/**
* Splits `text` into plain-text and anchor segments, wrapping any detected
* URL in an anchor that opens in a new tab. Trailing punctuation is kept
* outside the link so sentences like "see https://signoz.io." stay clean.
*/
export function linkifyText(text: string): ReactNode {
if (!text) {
return text;
}
const segments: ReactNode[] = [];
let lastIndex = 0;
let key = 0;
const matches = text.matchAll(URL_REGEX);
for (const match of matches) {
const matchStart = match.index ?? 0;
const rawUrl = match[0];
const trailing = rawUrl.match(TRAILING_PUNCTUATION)?.[0] ?? '';
const url = trailing ? rawUrl.slice(0, -trailing.length) : rawUrl;
const href = url.startsWith('www.') ? `https://${url}` : url;
if (matchStart > lastIndex) {
segments.push(
<Fragment key={key}>{text.slice(lastIndex, matchStart)}</Fragment>,
);
key += 1;
}
segments.push(
<a
key={key}
href={href}
rel="noopener noreferrer"
target="_blank"
onClick={stopPropagation}
>
{url}
</a>,
);
key += 1;
if (trailing) {
segments.push(<Fragment key={key}>{trailing}</Fragment>);
key += 1;
}
lastIndex = matchStart + rawUrl.length;
}
if (lastIndex < text.length) {
segments.push(<Fragment key={key}>{text.slice(lastIndex)}</Fragment>);
}
return segments;
}

View File

@@ -117,16 +117,8 @@ var podsSpec = checkSpec{
DocumentationLink: docLinkKubeletStatsReceiver,
},
{
Component: componentK8sClusterReceiver,
DefaultMetrics: []string{
"k8s.pod.phase",
"k8s.container.restarts", // pod restart count (default-on)
},
OptionalMetrics: []string{
// kubectl-style pod display status (default-off in the receiver).
"k8s.pod.status_reason",
"k8s.container.status.reason",
},
Component: componentK8sClusterReceiver,
DefaultMetrics: []string{"k8s.pod.phase"},
DocumentationLink: docLinkK8sClusterReceiver,
},
{
@@ -157,10 +149,6 @@ var nodesSpec = checkSpec{
// By default, only k8s.node.condition_ready is enabled. (Check https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/4f9a578b210a6dcb9f9bf47942f27208b5765298/receiver/k8sclusterreceiver/metadata.yaml#L802)
"k8s.pod.phase", // pod counts per node by phase
},
OptionalMetrics: []string{
"k8s.pod.status_reason",
"k8s.container.status.reason",
},
DocumentationLink: docLinkK8sClusterReceiver,
},
{
@@ -194,10 +182,6 @@ var deploymentsSpec = checkSpec{
"k8s.deployment.desired",
"k8s.deployment.available",
},
OptionalMetrics: []string{
"k8s.pod.status_reason",
"k8s.container.status.reason",
},
DocumentationLink: docLinkK8sClusterReceiver,
},
{
@@ -236,10 +220,6 @@ var daemonsetsSpec = checkSpec{
"k8s.daemonset.desired_scheduled_nodes",
"k8s.daemonset.current_scheduled_nodes",
},
OptionalMetrics: []string{
"k8s.pod.status_reason",
"k8s.container.status.reason",
},
DocumentationLink: docLinkK8sClusterReceiver,
},
{
@@ -278,10 +258,6 @@ var statefulsetsSpec = checkSpec{
"k8s.statefulset.desired_pods",
"k8s.statefulset.current_pods",
},
OptionalMetrics: []string{
"k8s.pod.status_reason",
"k8s.container.status.reason",
},
DocumentationLink: docLinkK8sClusterReceiver,
},
{
@@ -322,10 +298,6 @@ var jobsSpec = checkSpec{
"k8s.job.failed_pods",
"k8s.job.successful_pods",
},
OptionalMetrics: []string{
"k8s.pod.status_reason",
"k8s.container.status.reason",
},
DocumentationLink: docLinkK8sClusterReceiver,
},
{
@@ -352,12 +324,8 @@ var namespacesSpec = checkSpec{
DocumentationLink: docLinkKubeletStatsReceiver,
},
{
Component: componentK8sClusterReceiver,
DefaultMetrics: []string{"k8s.pod.phase"},
OptionalMetrics: []string{
"k8s.pod.status_reason",
"k8s.container.status.reason",
},
Component: componentK8sClusterReceiver,
DefaultMetrics: []string{"k8s.pod.phase"},
DocumentationLink: docLinkK8sClusterReceiver,
},
{
@@ -392,10 +360,6 @@ var clustersSpec = checkSpec{
"k8s.node.condition_ready", // node counts by readiness
"k8s.pod.phase", // pod counts per cluster by phase
},
OptionalMetrics: []string{
"k8s.pod.status_reason",
"k8s.container.status.reason",
},
DocumentationLink: docLinkK8sClusterReceiver,
},
{

View File

@@ -20,7 +20,6 @@ func buildClusterRecords(
metadataMap map[string]map[string]string,
nodeConditionCountsMap map[string]nodeConditionCounts,
podPhaseCountsMap map[string]podPhaseCounts,
podStatusCounts map[string]podStatusCounts,
) []inframonitoringtypes.ClusterRecord {
metricsMap := parseFullQueryResponse(resp, groupBy)
@@ -70,10 +69,6 @@ func buildClusterRecords(
}
}
if podStatusCountsForGroup, ok := podStatusCounts[compositeKey]; ok {
record.PodCountsByStatus = podStatusCountsToResponse(podStatusCountsForGroup)
}
if attrs, ok := metadataMap[compositeKey]; ok {
for k, v := range attrs {
record.Meta[k] = v

View File

@@ -27,8 +27,6 @@ var clustersTableMetricNamesList = []string{
"k8s.node.allocatable_memory",
"k8s.node.condition_ready", //TODO(nikhilmantri0902): should these metrics be used to count groups k8s.node.condition_ready and k8s.pod.phase
"k8s.pod.phase",
"k8s.pod.status_reason",
"k8s.container.status.reason",
}
var clusterAttrKeysForMetadata = []string{

View File

@@ -19,7 +19,6 @@ func buildDaemonSetRecords(
groupBy []qbtypes.GroupByKey,
metadataMap map[string]map[string]string,
phaseCounts map[string]podPhaseCounts,
podStatusCounts map[string]podStatusCounts,
) []inframonitoringtypes.DaemonSetRecord {
metricsMap := parseFullQueryResponse(resp, groupBy)
@@ -78,10 +77,6 @@ func buildDaemonSetRecords(
}
}
if podStatusCountsForGroup, ok := podStatusCounts[compositeKey]; ok {
record.PodCountsByStatus = podStatusCountsToResponse(podStatusCountsForGroup)
}
if attrs, ok := metadataMap[compositeKey]; ok {
for k, v := range attrs {
record.Meta[k] = v

View File

@@ -23,8 +23,6 @@ var daemonSetNameGroupByKey = qbtypes.GroupByKey{
// response to short-circuit cleanly when the phase metric is absent.
var daemonSetsTableMetricNamesList = []string{
"k8s.pod.phase",
"k8s.pod.status_reason",
"k8s.container.status.reason",
"k8s.pod.cpu.usage",
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",

View File

@@ -19,7 +19,6 @@ func buildDeploymentRecords(
groupBy []qbtypes.GroupByKey,
metadataMap map[string]map[string]string,
phaseCounts map[string]podPhaseCounts,
podStatusCounts map[string]podStatusCounts,
) []inframonitoringtypes.DeploymentRecord {
metricsMap := parseFullQueryResponse(resp, groupBy)
@@ -78,10 +77,6 @@ func buildDeploymentRecords(
}
}
if podStatusCountsForGroup, ok := podStatusCounts[compositeKey]; ok {
record.PodCountsByStatus = podStatusCountsToResponse(podStatusCountsForGroup)
}
if attrs, ok := metadataMap[compositeKey]; ok {
for k, v := range attrs {
record.Meta[k] = v

View File

@@ -23,8 +23,6 @@ var deploymentNameGroupByKey = qbtypes.GroupByKey{
// response to short-circuit cleanly when the phase metric is absent.
var deploymentsTableMetricNamesList = []string{
"k8s.pod.phase",
"k8s.pod.status_reason",
"k8s.container.status.reason",
"k8s.pod.cpu.usage",
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",

View File

@@ -440,30 +440,6 @@ func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter,
return whereClause.WhereClause, nil
}
// mergeQueryWarnings combines any number of query warnings. It is nil-safe and
// skips nil entries. The first non-nil warning becomes the primary; each
// subsequent one contributes its message (as an additional warning) and its
// own additional warnings. Returns nil when all inputs are nil.
func mergeQueryWarnings(warnings ...*qbtypes.QueryWarnData) *qbtypes.QueryWarnData {
var merged *qbtypes.QueryWarnData
for _, w := range warnings {
if w == nil {
continue
}
if merged == nil {
// Copy so we don't mutate the caller's warning.
primary := *w
merged = &primary
continue
}
if w.Message != "" {
merged.Warnings = append(merged.Warnings, qbtypes.QueryWarnDataAdditional{Message: w.Message})
}
merged.Warnings = append(merged.Warnings, w.Warnings...)
}
return merged
}
// NOTE: this method is not specific to infra monitoring — it queries attributes_metadata generically.
// Consider moving to telemetryMetaStore when a second use case emerges.
//

View File

@@ -26,60 +26,6 @@ type podPhaseCounts struct {
Unknown int
}
// podStatusCounts holds per-group pod counts bucketed by latest kubectl-style
// display status in window. Mirrors inframonitoringtypes.PodCountsByStatus.
type podStatusCounts struct {
// Phase fallback.
Pending int
Running int
Failed int
Unknown int
// Container-level reasons.
CrashLoopBackOff int
ImagePullBackOff int
ErrImagePull int
CreateContainerConfigError int
ContainerCreating int
OOMKilled int
Completed int
Error int
ContainerCannotRun int
// Pod-level reasons.
Evicted int
NodeAffinity int
NodeLost int
Shutdown int
UnexpectedAdmissionError int
}
// podStatusCountsToResponse copies the internal per-group status counts into the
// public response struct. Shared by every entity that surfaces pod status
// counts (pods, nodes, namespaces, clusters, workloads).
func podStatusCountsToResponse(podStatuses podStatusCounts) inframonitoringtypes.PodCountsByStatus {
return inframonitoringtypes.PodCountsByStatus{
Pending: podStatuses.Pending,
Running: podStatuses.Running,
Failed: podStatuses.Failed,
Unknown: podStatuses.Unknown,
CrashLoopBackOff: podStatuses.CrashLoopBackOff,
ImagePullBackOff: podStatuses.ImagePullBackOff,
ErrImagePull: podStatuses.ErrImagePull,
CreateContainerConfigError: podStatuses.CreateContainerConfigError,
ContainerCreating: podStatuses.ContainerCreating,
OOMKilled: podStatuses.OOMKilled,
Completed: podStatuses.Completed,
Error: podStatuses.Error,
ContainerCannotRun: podStatuses.ContainerCannotRun,
Evicted: podStatuses.Evicted,
NodeAffinity: podStatuses.NodeAffinity,
NodeLost: podStatuses.NodeLost,
Shutdown: podStatuses.Shutdown,
UnexpectedAdmissionError: podStatuses.UnexpectedAdmissionError,
}
}
// nodeConditionCounts holds per-group node counts bucketed by latest condition_ready in window.
type nodeConditionCounts struct {
Ready int

View File

@@ -19,7 +19,6 @@ func buildJobRecords(
groupBy []qbtypes.GroupByKey,
metadataMap map[string]map[string]string,
phaseCounts map[string]podPhaseCounts,
podStatusCounts map[string]podStatusCounts,
) []inframonitoringtypes.JobRecord {
metricsMap := parseFullQueryResponse(resp, groupBy)
@@ -86,10 +85,6 @@ func buildJobRecords(
}
}
if podStatusCountsForGroup, ok := podStatusCounts[compositeKey]; ok {
record.PodCountsByStatus = podStatusCountsToResponse(podStatusCountsForGroup)
}
if attrs, ok := metadataMap[compositeKey]; ok {
for k, v := range attrs {
record.Meta[k] = v

View File

@@ -23,8 +23,6 @@ var jobNameGroupByKey = qbtypes.GroupByKey{
// response to short-circuit cleanly when the phase metric is absent.
var jobsTableMetricNamesList = []string{
"k8s.pod.phase",
"k8s.pod.status_reason",
"k8s.container.status.reason",
"k8s.pod.cpu.usage",
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",

View File

@@ -309,19 +309,9 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
return nil, err
}
statusCounts, statusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
restartCounts, err := m.getPerGroupPodRestartCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
isPodUIDInGroupBy := isKeyInGroupByAttrs(req.GroupBy, podUIDAttrKey)
resp.Records = buildPodRecords(isPodUIDInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, statusCounts, restartCounts, req.End)
resp.Warning = mergeQueryWarnings(queryResp.Warning, statusWarning)
resp.Records = buildPodRecords(isPodUIDInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, req.End)
resp.Warning = queryResp.Warning
return resp, nil
}
@@ -402,14 +392,9 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
return nil, err
}
podStatusCounts, podStatusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
isNodeNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, inframonitoringtypes.NodeNameAttrKey)
resp.Records = buildNodeRecords(isNodeNameInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCounts, podPhaseCounts, podStatusCounts)
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
resp.Records = buildNodeRecords(isNodeNameInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCounts, podPhaseCounts)
resp.Warning = queryResp.Warning
return resp, nil
}
@@ -485,13 +470,8 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
return nil, err
}
podStatusCounts, podStatusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
resp.Records = buildNamespaceRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
resp.Records = buildNamespaceRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
resp.Warning = queryResp.Warning
return resp, nil
}
@@ -574,13 +554,8 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
return nil, err
}
podStatusCounts, podStatusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
resp.Records = buildClusterRecords(queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCountsMap, podPhaseCountsMap, podStatusCounts)
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
resp.Records = buildClusterRecords(queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCountsMap, podPhaseCountsMap)
resp.Warning = queryResp.Warning
return resp, nil
}
@@ -740,13 +715,8 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
return nil, err
}
podStatusCounts, podStatusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
resp.Records = buildDeploymentRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
resp.Records = buildDeploymentRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
resp.Warning = queryResp.Warning
return resp, nil
}
@@ -830,13 +800,8 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
return nil, err
}
podStatusCounts, podStatusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
resp.Records = buildStatefulSetRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
resp.Records = buildStatefulSetRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
resp.Warning = queryResp.Warning
return resp, nil
}
@@ -920,13 +885,8 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
return nil, err
}
podStatusCounts, podStatusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
resp.Records = buildJobRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
resp.Records = buildJobRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
resp.Warning = queryResp.Warning
return resp, nil
}
@@ -1010,13 +970,8 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
return nil, err
}
podStatusCounts, podStatusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
if err != nil {
return nil, err
}
resp.Records = buildDaemonSetRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
resp.Records = buildDaemonSetRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
resp.Warning = queryResp.Warning
return resp, nil
}

View File

@@ -18,7 +18,6 @@ func buildNamespaceRecords(
groupBy []qbtypes.GroupByKey,
metadataMap map[string]map[string]string,
phaseCounts map[string]podPhaseCounts,
podStatusCounts map[string]podStatusCounts,
) []inframonitoringtypes.NamespaceRecord {
metricsMap := parseFullQueryResponse(resp, groupBy)
@@ -53,10 +52,6 @@ func buildNamespaceRecords(
}
}
if podStatusCountsForGroup, ok := podStatusCounts[compositeKey]; ok {
record.PodCountsByStatus = podStatusCountsToResponse(podStatusCountsForGroup)
}
if attrs, ok := metadataMap[compositeKey]; ok {
for k, v := range attrs {
record.Meta[k] = v

View File

@@ -23,8 +23,6 @@ var namespacesTableMetricNamesList = []string{
"k8s.pod.cpu.usage",
"k8s.pod.memory.working_set",
"k8s.pod.phase",
"k8s.pod.status_reason",
"k8s.container.status.reason",
}
var namespaceAttrKeysForMetadata = []string{

View File

@@ -26,7 +26,6 @@ func buildNodeRecords(
metadataMap map[string]map[string]string,
nodeConditionCounts map[string]nodeConditionCounts,
podPhaseCounts map[string]podPhaseCounts,
podStatusCounts map[string]podStatusCounts,
) []inframonitoringtypes.NodeRecord {
metricsMap := parseFullQueryResponse(resp, groupBy)
@@ -87,10 +86,6 @@ func buildNodeRecords(
}
}
if podStatusCountsForGroup, ok := podStatusCounts[compositeKey]; ok {
record.PodCountsByStatus = podStatusCountsToResponse(podStatusCountsForGroup)
}
if attrs, ok := metadataMap[compositeKey]; ok {
for k, v := range attrs {
record.Meta[k] = v

View File

@@ -26,8 +26,6 @@ var nodesTableMetricNamesList = []string{
"k8s.node.allocatable_memory",
"k8s.node.condition_ready",
"k8s.pod.phase",
"k8s.pod.status_reason",
"k8s.container.status.reason",
}
var nodeAttrKeysForMetadata = []string{

View File

@@ -26,8 +26,6 @@ func buildPodRecords(
groupBy []qbtypes.GroupByKey,
metadataMap map[string]map[string]string,
phaseCounts map[string]podPhaseCounts,
statusCounts map[string]podStatusCounts,
restartCounts map[string]int64,
reqEnd int64,
) []inframonitoringtypes.PodRecord {
metricsMap := parseFullQueryResponse(resp, groupBy)
@@ -40,8 +38,6 @@ func buildPodRecords(
record := inframonitoringtypes.PodRecord{ // initialize with default values
PodUID: podUID,
PodPhase: inframonitoringtypes.PodPhaseNoData,
PodStatus: inframonitoringtypes.PodStatusNoData,
PodRestarts: -1,
PodCPU: -1,
PodCPURequest: -1,
PodCPULimit: -1,
@@ -99,57 +95,6 @@ func buildPodRecords(
}
}
if statusCountsForGroup, ok := statusCounts[compositeKey]; ok {
record.PodCountsByStatus = podStatusCountsToResponse(statusCountsForGroup)
// In list mode each group is one pod; the count==1 bucket identifies the status.
if isPodUIDInGroupBy {
switch {
case statusCountsForGroup.Pending == 1:
record.PodStatus = inframonitoringtypes.PodStatusPending
case statusCountsForGroup.Running == 1:
record.PodStatus = inframonitoringtypes.PodStatusRunning
case statusCountsForGroup.Failed == 1:
record.PodStatus = inframonitoringtypes.PodStatusFailed
case statusCountsForGroup.Unknown == 1:
record.PodStatus = inframonitoringtypes.PodStatusUnknown
case statusCountsForGroup.CrashLoopBackOff == 1:
record.PodStatus = inframonitoringtypes.PodStatusCrashLoopBackOff
case statusCountsForGroup.ImagePullBackOff == 1:
record.PodStatus = inframonitoringtypes.PodStatusImagePullBackOff
case statusCountsForGroup.ErrImagePull == 1:
record.PodStatus = inframonitoringtypes.PodStatusErrImagePull
case statusCountsForGroup.CreateContainerConfigError == 1:
record.PodStatus = inframonitoringtypes.PodStatusCreateContainerConfigError
case statusCountsForGroup.ContainerCreating == 1:
record.PodStatus = inframonitoringtypes.PodStatusContainerCreating
case statusCountsForGroup.OOMKilled == 1:
record.PodStatus = inframonitoringtypes.PodStatusOOMKilled
case statusCountsForGroup.Completed == 1:
record.PodStatus = inframonitoringtypes.PodStatusCompleted
case statusCountsForGroup.Error == 1:
record.PodStatus = inframonitoringtypes.PodStatusError
case statusCountsForGroup.ContainerCannotRun == 1:
record.PodStatus = inframonitoringtypes.PodStatusContainerCannotRun
case statusCountsForGroup.Evicted == 1:
record.PodStatus = inframonitoringtypes.PodStatusEvicted
case statusCountsForGroup.NodeAffinity == 1:
record.PodStatus = inframonitoringtypes.PodStatusNodeAffinity
case statusCountsForGroup.NodeLost == 1:
record.PodStatus = inframonitoringtypes.PodStatusNodeLost
case statusCountsForGroup.Shutdown == 1:
record.PodStatus = inframonitoringtypes.PodStatusShutdown
case statusCountsForGroup.UnexpectedAdmissionError == 1:
record.PodStatus = inframonitoringtypes.PodStatusUnexpectedAdmissionError
}
}
}
// Restart count: pod's own sum (list mode) or group total (grouped mode).
if restartCountForGroup, ok := restartCounts[compositeKey]; ok {
record.PodRestarts = restartCountForGroup
}
if attrs, ok := metadataMap[compositeKey]; ok {
// podAge only makes sense when pod uid is in groupBy. Otherwise the
// group can contain multiple pods with different start times.
@@ -389,519 +334,3 @@ func (m *module) getPerGroupPodPhaseCounts(
}
return result, nil
}
// getPerGroupPodStatusCountsWithReqMetricChecks gates getPerGroupPodStatusCounts
// on the required metrics being present. If any of podStatusMetricNamesList has
// never been reported, it skips the query and returns a warning instead (the
// status query would otherwise silently degrade to bare phase). Otherwise it
// runs the query. The returned counts map is empty (never nil) when gated off.
func (m *module) getPerGroupPodStatusCountsWithReqMetricChecks(
ctx context.Context,
start, end int64,
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
pageGroups []map[string]string,
) (map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
present, err := m.getMetricsExistence(ctx, podStatusMetricNamesList)
if err != nil {
return nil, nil, err
}
var missing []string
for _, name := range podStatusMetricNamesList {
if !present[name] {
missing = append(missing, name)
}
}
if len(missing) > 0 {
warning := &qbtypes.QueryWarnData{
Message: fmt.Sprintf(
"Pod status could not be computed: required metric(s) not found: %s. "+
"Enable the optional k8s.pod.status_reason and k8s.container.status.reason "+
"metrics in the k8s_cluster receiver to see pod statuses.",
strings.Join(missing, ", "),
),
Url: docLinkK8sClusterReceiver,
}
return map[string]podStatusCounts{}, warning, nil
}
counts, err := m.getPerGroupPodStatusCounts(ctx, start, end, filter, groupBy, pageGroups)
if err != nil {
return nil, nil, err
}
return counts, nil, nil
}
// getPerGroupPodStatusCounts computes per-group pod counts bucketed by each
// pod's latest kubectl-style display status in the requested window. Caller
// must ensure the required metrics exist (getPerGroupPodStatusCountsWithReqMetricChecks).
// Pipeline (mirrors getPerGroupPodPhaseCounts, more CTEs):
//
// phase_fps / phase_per_pod: latest k8s.pod.phase per pod (+ groupBy cols).
// pod_reason_fps / pod_reason_per_pod: latest k8s.pod.status_reason per pod.
// container_reason_fps / container_reason_per_pod:
// highest-priority active k8s.container.status.reason per pod.
// pod_status: display status per pod (container > pod reason > phase).
// countPodsPerStatus: per-group uniqExactIf into the fixed status buckets.
//
// Groups absent from the result map have implicit zero counts (caller default).
func (m *module) getPerGroupPodStatusCounts(
ctx context.Context,
start, end int64,
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
pageGroups []map[string]string,
) (map[string]podStatusCounts, error) {
if len(pageGroups) == 0 || len(groupBy) == 0 {
return map[string]podStatusCounts{}, nil
}
// Merge user filter with page-groups IN clauses.
userFilterExpr := ""
if filter != nil {
userFilterExpr = filter.Expression
}
mergedFilterExpr := mergeFilterExpressions(userFilterExpr, buildPageGroupsFilterExpr(pageGroups))
samplesStartMs, flooredEndMs, tsAdjustedStart, _, localTimeSeriesTable, distributedSamplesTable, _ := alignedMetricWindow(start, end)
valueCol := telemetrymetrics.ValueColumnForSamplesTable(distributedSamplesTable)
// Build the merged filter clause once; it's identical across the three fps
// CTEs, and buildFilterClause hits the metadata store + parses the
// expression, so we don't want to repeat it per CTE. AddWhereClause only
// reads the clause, so the same instance is safe to attach to each builder.
var (
filterClause *sqlbuilder.WhereClause
err error
)
if mergedFilterExpr != "" {
filterClause, err = m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
if err != nil {
return nil, err
}
}
// ----- phase_fps (carries groupBy cols) -----
phaseFps := sqlbuilder.NewSelectBuilder()
phaseFpsCols := []string{
"fingerprint",
fmt.Sprintf("JSONExtractString(labels, %s) AS pod_uid", phaseFps.Var(podUIDAttrKey)),
}
for _, key := range groupBy {
phaseFpsCols = append(phaseFpsCols,
fmt.Sprintf("JSONExtractString(labels, %s) AS %s", phaseFps.Var(key.Name), quoteIdentifier(key.Name)),
)
}
phaseFps.Select(phaseFpsCols...)
phaseFps.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, localTimeSeriesTable))
phaseFps.Where(
phaseFps.E("metric_name", podPhaseMetricName),
phaseFps.GE("unix_milli", tsAdjustedStart),
phaseFps.LE("unix_milli", flooredEndMs),
)
if filterClause != nil {
phaseFps.AddWhereClause(filterClause)
}
phaseFpsGroupBy := []string{"fingerprint", "pod_uid"}
for _, key := range groupBy {
phaseFpsGroupBy = append(phaseFpsGroupBy, quoteIdentifier(key.Name))
}
phaseFps.GroupBy(phaseFpsGroupBy...)
phaseFpsSQL, phaseFpsArgs := phaseFps.BuildWithFlavor(sqlbuilder.ClickHouse)
// ----- phase_per_pod -----
phasePerPod := sqlbuilder.NewSelectBuilder()
phasePerPodCols := []string{"fps.pod_uid AS pod_uid"}
for _, key := range groupBy {
col := quoteIdentifier(key.Name)
phasePerPodCols = append(phasePerPodCols, fmt.Sprintf("argMax(fps.%s, samples.unix_milli) AS %s", col, col))
}
phasePerPodCols = append(phasePerPodCols, fmt.Sprintf("argMax(samples.%s, samples.unix_milli) AS phase_value", valueCol))
phasePerPod.Select(phasePerPodCols...)
phasePerPod.From(fmt.Sprintf(
"%s.%s AS samples INNER JOIN phase_fps AS fps ON samples.fingerprint = fps.fingerprint",
telemetrymetrics.DBName, distributedSamplesTable,
))
phasePerPod.Where(
phasePerPod.E("samples.metric_name", podPhaseMetricName),
phasePerPod.GE("samples.unix_milli", samplesStartMs),
phasePerPod.L("samples.unix_milli", flooredEndMs),
"fps.pod_uid != ''",
)
phasePerPod.GroupBy("pod_uid")
phasePerPodSQL, phasePerPodArgs := phasePerPod.BuildWithFlavor(sqlbuilder.ClickHouse)
// ----- pod_reason_fps -----
podReasonFps := sqlbuilder.NewSelectBuilder()
podReasonFps.Select(
"fingerprint",
fmt.Sprintf("JSONExtractString(labels, %s) AS pod_uid", podReasonFps.Var(podUIDAttrKey)),
)
podReasonFps.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, localTimeSeriesTable))
podReasonFps.Where(
podReasonFps.E("metric_name", podStatusReasonMetricName),
podReasonFps.GE("unix_milli", tsAdjustedStart),
podReasonFps.LE("unix_milli", flooredEndMs),
)
if filterClause != nil {
podReasonFps.AddWhereClause(filterClause)
}
podReasonFps.GroupBy("fingerprint", "pod_uid")
podReasonFpsSQL, podReasonFpsArgs := podReasonFps.BuildWithFlavor(sqlbuilder.ClickHouse)
// ----- pod_reason_per_pod -----
podReasonPerPod := sqlbuilder.NewSelectBuilder()
podReasonPerPod.Select(
"fps.pod_uid AS pod_uid",
fmt.Sprintf("argMax(samples.%s, samples.unix_milli) AS reason_value", valueCol),
)
podReasonPerPod.From(fmt.Sprintf(
"%s.%s AS samples INNER JOIN pod_reason_fps AS fps ON samples.fingerprint = fps.fingerprint",
telemetrymetrics.DBName, distributedSamplesTable,
))
podReasonPerPod.Where(
podReasonPerPod.E("samples.metric_name", podStatusReasonMetricName),
podReasonPerPod.GE("samples.unix_milli", samplesStartMs),
podReasonPerPod.L("samples.unix_milli", flooredEndMs),
"fps.pod_uid != ''",
)
podReasonPerPod.GroupBy("pod_uid")
podReasonPerPodSQL, podReasonPerPodArgs := podReasonPerPod.BuildWithFlavor(sqlbuilder.ClickHouse)
// ----- container_reason_fps -----
containerReasonFps := sqlbuilder.NewSelectBuilder()
containerReasonFps.Select(
"fingerprint",
fmt.Sprintf("JSONExtractString(labels, %s) AS pod_uid", containerReasonFps.Var(podUIDAttrKey)),
fmt.Sprintf("JSONExtractString(labels, %s) AS container_name", containerReasonFps.Var(containerNameAttrKey)),
fmt.Sprintf("JSONExtractString(labels, %s) AS reason", containerReasonFps.Var(containerStatusReasonAttrKey)),
)
containerReasonFps.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, localTimeSeriesTable))
containerReasonFps.Where(
containerReasonFps.E("metric_name", containerStatusReasonMetricName),
containerReasonFps.GE("unix_milli", tsAdjustedStart),
containerReasonFps.LE("unix_milli", flooredEndMs),
)
if filterClause != nil {
containerReasonFps.AddWhereClause(filterClause)
}
containerReasonFps.GroupBy("fingerprint", "pod_uid", "container_name", "reason")
containerReasonFpsSQL, containerReasonFpsArgs := containerReasonFps.BuildWithFlavor(sqlbuilder.ClickHouse)
// ----- container_reason_per_pod -----
// Inner: latest value per (pod, container, reason) -> kills stale
// fingerprints from old container incarnations; keep only active (=1).
// Outer: highest-priority active reason per pod (waiting > terminated).
priorityCase := "CASE fps.reason " +
"WHEN 'CrashLoopBackOff' THEN 8 " +
"WHEN 'ImagePullBackOff' THEN 7 " +
"WHEN 'ErrImagePull' THEN 6 " +
"WHEN 'CreateContainerConfigError' THEN 5 " +
"WHEN 'ContainerCreating' THEN 4 " +
"WHEN 'OOMKilled' THEN 3 " +
"WHEN 'Error' THEN 2 " +
"WHEN 'ContainerCannotRun' THEN 1 " +
"ELSE 0 END"
containerInner := sqlbuilder.NewSelectBuilder()
containerInner.Select(
"fps.pod_uid AS pod_uid",
"fps.container_name AS container_name",
"fps.reason AS reason",
fmt.Sprintf("argMax(samples.%s, samples.unix_milli) AS is_active", valueCol),
priorityCase+" AS priority",
)
containerInner.From(fmt.Sprintf(
"%s.%s AS samples INNER JOIN container_reason_fps AS fps ON samples.fingerprint = fps.fingerprint",
telemetrymetrics.DBName, distributedSamplesTable,
))
containerInner.Where(
containerInner.E("samples.metric_name", containerStatusReasonMetricName),
containerInner.GE("samples.unix_milli", samplesStartMs),
containerInner.L("samples.unix_milli", flooredEndMs),
"fps.pod_uid != ''",
)
containerInner.GroupBy("fps.pod_uid", "fps.container_name", "fps.reason")
containerInner.Having("is_active = 1")
containerInnerSQL, containerInnerArgs := containerInner.BuildWithFlavor(sqlbuilder.ClickHouse)
containerPerPodSQL := fmt.Sprintf(
"SELECT pod_uid, argMax(reason, priority) AS active_reason FROM (%s) GROUP BY pod_uid",
containerInnerSQL,
)
// ----- pod_status (display status per pod) -----
// container reason > pod-level reason > phase fallback. Numeric literals
// match the k8s.pod.status_reason and k8s.pod.phase metric encodings.
displayStatusExpr := "multiIf(" +
"cr.active_reason != '', cr.active_reason, " +
"pr.reason_value = 1, 'Evicted', " +
"pr.reason_value = 2, 'NodeAffinity', " +
"pr.reason_value = 3, 'NodeLost', " +
"pr.reason_value = 4, 'Shutdown', " +
"pr.reason_value = 5, 'UnexpectedAdmissionError', " +
"pp.phase_value = 1, 'Pending', " +
"pp.phase_value = 2, 'Running', " +
"pp.phase_value = 3, 'Completed', " +
"pp.phase_value = 4, 'Failed', " +
"pp.phase_value = 5, 'Unknown', " +
"'Unknown')"
podStatusSelectCols := []string{"pp.pod_uid AS pod_uid"}
for _, key := range groupBy {
col := quoteIdentifier(key.Name)
podStatusSelectCols = append(podStatusSelectCols, fmt.Sprintf("pp.%s AS %s", col, col))
}
podStatusSelectCols = append(podStatusSelectCols, displayStatusExpr+" AS display_status")
podStatusSQL := fmt.Sprintf(
"SELECT %s FROM phase_per_pod AS pp "+
"LEFT JOIN pod_reason_per_pod AS pr ON pp.pod_uid = pr.pod_uid "+
"LEFT JOIN container_reason_per_pod AS cr ON pp.pod_uid = cr.pod_uid",
strings.Join(podStatusSelectCols, ", "),
)
// ----- countPodsPerStatus (outer SELECT) -----
// Fixed status order; MUST match the podStatusCounts assignment in the
// scan loop below.
statusCountCols := []string{
"uniqExactIf(pod_uid, display_status = 'Pending') AS pending_count",
"uniqExactIf(pod_uid, display_status = 'Running') AS running_count",
"uniqExactIf(pod_uid, display_status = 'Failed') AS failed_count",
"uniqExactIf(pod_uid, display_status = 'Unknown') AS unknown_count",
"uniqExactIf(pod_uid, display_status = 'CrashLoopBackOff') AS crash_loop_back_off_count",
"uniqExactIf(pod_uid, display_status = 'ImagePullBackOff') AS image_pull_back_off_count",
"uniqExactIf(pod_uid, display_status = 'ErrImagePull') AS err_image_pull_count",
"uniqExactIf(pod_uid, display_status = 'CreateContainerConfigError') AS create_container_config_error_count",
"uniqExactIf(pod_uid, display_status = 'ContainerCreating') AS container_creating_count",
"uniqExactIf(pod_uid, display_status = 'OOMKilled') AS oom_killed_count",
"uniqExactIf(pod_uid, display_status = 'Completed') AS completed_count",
"uniqExactIf(pod_uid, display_status = 'Error') AS error_count",
"uniqExactIf(pod_uid, display_status = 'ContainerCannotRun') AS container_cannot_run_count",
"uniqExactIf(pod_uid, display_status = 'Evicted') AS evicted_count",
"uniqExactIf(pod_uid, display_status = 'NodeAffinity') AS node_affinity_count",
"uniqExactIf(pod_uid, display_status = 'NodeLost') AS node_lost_count",
"uniqExactIf(pod_uid, display_status = 'Shutdown') AS shutdown_count",
"uniqExactIf(pod_uid, display_status = 'UnexpectedAdmissionError') AS unexpected_admission_error_count",
}
countSelectCols := make([]string, 0, len(groupBy)+len(statusCountCols))
countGroupBy := make([]string, 0, len(groupBy))
for _, key := range groupBy {
col := quoteIdentifier(key.Name)
countSelectCols = append(countSelectCols, col)
countGroupBy = append(countGroupBy, col)
}
countSelectCols = append(countSelectCols, statusCountCols...)
countSQL := fmt.Sprintf(
"SELECT %s FROM pod_status GROUP BY %s",
strings.Join(countSelectCols, ", "),
strings.Join(countGroupBy, ", "),
)
// Combine CTEs + outer. Arg order mirrors CTE declaration order.
cteFragments := []string{
fmt.Sprintf("phase_fps AS (%s)", phaseFpsSQL),
fmt.Sprintf("phase_per_pod AS (%s)", phasePerPodSQL),
fmt.Sprintf("pod_reason_fps AS (%s)", podReasonFpsSQL),
fmt.Sprintf("pod_reason_per_pod AS (%s)", podReasonPerPodSQL),
fmt.Sprintf("container_reason_fps AS (%s)", containerReasonFpsSQL),
fmt.Sprintf("container_reason_per_pod AS (%s)", containerPerPodSQL),
fmt.Sprintf("pod_status AS (%s)", podStatusSQL),
}
finalSQL := querybuilder.CombineCTEs(cteFragments) + countSQL
finalArgs := querybuilder.PrependArgs([][]any{
phaseFpsArgs, phasePerPodArgs,
podReasonFpsArgs, podReasonPerPodArgs,
containerReasonFpsArgs, containerInnerArgs,
}, nil)
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
if err != nil {
return nil, err
}
defer rows.Close()
result := make(map[string]podStatusCounts)
for rows.Next() {
groupVals := make([]string, len(groupBy))
counts := make([]uint64, len(statusCountCols))
scanPtrs := make([]any, 0, len(groupBy)+len(statusCountCols))
for i := range groupVals {
scanPtrs = append(scanPtrs, &groupVals[i])
}
for i := range counts {
scanPtrs = append(scanPtrs, &counts[i])
}
if err := rows.Scan(scanPtrs...); err != nil {
return nil, err
}
result[compositeKeyFromList(groupVals)] = podStatusCounts{
Pending: int(counts[0]),
Running: int(counts[1]),
Failed: int(counts[2]),
Unknown: int(counts[3]),
CrashLoopBackOff: int(counts[4]),
ImagePullBackOff: int(counts[5]),
ErrImagePull: int(counts[6]),
CreateContainerConfigError: int(counts[7]),
ContainerCreating: int(counts[8]),
OOMKilled: int(counts[9]),
Completed: int(counts[10]),
Error: int(counts[11]),
ContainerCannotRun: int(counts[12]),
Evicted: int(counts[13]),
NodeAffinity: int(counts[14]),
NodeLost: int(counts[15]),
Shutdown: int(counts[16]),
UnexpectedAdmissionError: int(counts[17]),
}
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
}
// getPerGroupPodRestartCounts computes the absolute pod restart count per group
// from k8s.container.restarts (default-enabled, so no existence gate). In list
// mode (groupBy=pod_uid) each group is one pod -> its restart count; in grouped
// mode it's the summed restarts across all pods in the group. Mirrors DD:
// argMax(value, unix_milli) per (pod, container) takes the current cumulative
// count from the latest incarnation (handling fingerprint staleness/pruning),
// then sum.
//
// restart_fps: fp ↔ (pod_uid, container_name, groupBy cols) from time_series.
// container_restarts: INNER JOIN samples, latest restartCount per (pod, container).
// (outer): per-group sum(restart_count).
//
// Groups absent from the result map have no data (caller default).
func (m *module) getPerGroupPodRestartCounts(
ctx context.Context,
start, end int64,
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
pageGroups []map[string]string,
) (map[string]int64, error) {
if len(pageGroups) == 0 || len(groupBy) == 0 {
return map[string]int64{}, nil
}
// Merge user filter with page-groups IN clauses.
userFilterExpr := ""
if filter != nil {
userFilterExpr = filter.Expression
}
mergedFilterExpr := mergeFilterExpressions(userFilterExpr, buildPageGroupsFilterExpr(pageGroups))
samplesStartMs, flooredEndMs, tsAdjustedStart, _, localTimeSeriesTable, distributedSamplesTable, _ := alignedMetricWindow(start, end)
valueCol := telemetrymetrics.ValueColumnForSamplesTable(distributedSamplesTable)
var (
filterClause *sqlbuilder.WhereClause
err error
)
if mergedFilterExpr != "" {
filterClause, err = m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
if err != nil {
return nil, err
}
}
// ----- restart_fps (carries groupBy cols) -----
restartFps := sqlbuilder.NewSelectBuilder()
restartFpsCols := []string{
"fingerprint",
fmt.Sprintf("JSONExtractString(labels, %s) AS pod_uid", restartFps.Var(podUIDAttrKey)),
fmt.Sprintf("JSONExtractString(labels, %s) AS container_name", restartFps.Var(containerNameAttrKey)),
}
for _, key := range groupBy {
restartFpsCols = append(restartFpsCols,
fmt.Sprintf("JSONExtractString(labels, %s) AS %s", restartFps.Var(key.Name), quoteIdentifier(key.Name)),
)
}
restartFps.Select(restartFpsCols...)
restartFps.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, localTimeSeriesTable))
restartFps.Where(
restartFps.E("metric_name", containerRestartsMetricName),
restartFps.GE("unix_milli", tsAdjustedStart),
restartFps.LE("unix_milli", flooredEndMs),
)
if filterClause != nil {
restartFps.AddWhereClause(filterClause)
}
restartFpsGroupBy := []string{"fingerprint", "pod_uid", "container_name"}
for _, key := range groupBy {
restartFpsGroupBy = append(restartFpsGroupBy, quoteIdentifier(key.Name))
}
restartFps.GroupBy(restartFpsGroupBy...)
restartFpsSQL, restartFpsArgs := restartFps.BuildWithFlavor(sqlbuilder.ClickHouse)
// ----- container_restarts (latest cumulative count per container) -----
containerRestarts := sqlbuilder.NewSelectBuilder()
containerRestartsCols := []string{
"fps.pod_uid AS pod_uid",
"fps.container_name AS container_name",
}
for _, key := range groupBy {
col := quoteIdentifier(key.Name)
containerRestartsCols = append(containerRestartsCols, fmt.Sprintf("argMax(fps.%s, samples.unix_milli) AS %s", col, col))
}
containerRestartsCols = append(containerRestartsCols, fmt.Sprintf("argMax(samples.%s, samples.unix_milli) AS restart_count", valueCol))
containerRestarts.Select(containerRestartsCols...)
containerRestarts.From(fmt.Sprintf(
"%s.%s AS samples INNER JOIN restart_fps AS fps ON samples.fingerprint = fps.fingerprint",
telemetrymetrics.DBName, distributedSamplesTable,
))
containerRestarts.Where(
containerRestarts.E("samples.metric_name", containerRestartsMetricName),
containerRestarts.GE("samples.unix_milli", samplesStartMs),
containerRestarts.L("samples.unix_milli", flooredEndMs),
"fps.pod_uid != ''",
)
containerRestarts.GroupBy("fps.pod_uid", "fps.container_name")
containerRestartsSQL, containerRestartsArgs := containerRestarts.BuildWithFlavor(sqlbuilder.ClickHouse)
// ----- outer: per-group sum across containers (hence across pods) -----
sumSelectCols := make([]string, 0, len(groupBy)+1)
sumGroupBy := make([]string, 0, len(groupBy))
for _, key := range groupBy {
col := quoteIdentifier(key.Name)
sumSelectCols = append(sumSelectCols, col)
sumGroupBy = append(sumGroupBy, col)
}
sumSelectCols = append(sumSelectCols, "sum(restart_count) AS total_restarts")
sumSQL := fmt.Sprintf(
"SELECT %s FROM container_restarts GROUP BY %s",
strings.Join(sumSelectCols, ", "),
strings.Join(sumGroupBy, ", "),
)
cteFragments := []string{
fmt.Sprintf("restart_fps AS (%s)", restartFpsSQL),
fmt.Sprintf("container_restarts AS (%s)", containerRestartsSQL),
}
finalSQL := querybuilder.CombineCTEs(cteFragments) + sumSQL
finalArgs := querybuilder.PrependArgs([][]any{restartFpsArgs, containerRestartsArgs}, nil)
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
if err != nil {
return nil, err
}
defer rows.Close()
result := make(map[string]int64)
for rows.Next() {
groupVals := make([]string, len(groupBy))
var totalRestarts float64
scanPtrs := make([]any, 0, len(groupBy)+1)
for i := range groupVals {
scanPtrs = append(scanPtrs, &groupVals[i])
}
scanPtrs = append(scanPtrs, &totalRestarts)
if err := rows.Scan(scanPtrs...); err != nil {
return nil, err
}
result[compositeKeyFromList(groupVals)] = int64(totalRestarts)
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
}

View File

@@ -8,26 +8,11 @@ import (
)
const (
podUIDAttrKey = "k8s.pod.uid"
podStartTimeAttrKey = "k8s.pod.start_time"
containerNameAttrKey = "k8s.container.name"
containerStatusReasonAttrKey = "k8s.container.status.reason"
podPhaseMetricName = "k8s.pod.phase"
podStatusReasonMetricName = "k8s.pod.status_reason"
containerStatusReasonMetricName = "k8s.container.status.reason"
containerRestartsMetricName = "k8s.container.restarts"
podUIDAttrKey = "k8s.pod.uid"
podStartTimeAttrKey = "k8s.pod.start_time"
podPhaseMetricName = "k8s.pod.phase"
)
// podStatusMetricNamesList are the metrics required to derive the kubectl-style
// pod display status. All three must be present (getMetricsExistence gate)
// before getPerGroupPodStatusCounts runs.
var podStatusMetricNamesList = []string{
podPhaseMetricName,
podStatusReasonMetricName,
containerStatusReasonMetricName,
}
var podUIDGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: podUIDAttrKey,
@@ -44,9 +29,6 @@ var podsTableMetricNamesList = []string{
"k8s.pod.memory_request_utilization",
"k8s.pod.memory_limit_utilization",
"k8s.pod.phase",
"k8s.pod.status_reason",
"k8s.container.status.reason",
"k8s.container.restarts",
}
var podAttrKeysForMetadata = []string{

View File

@@ -19,7 +19,6 @@ func buildStatefulSetRecords(
groupBy []qbtypes.GroupByKey,
metadataMap map[string]map[string]string,
phaseCounts map[string]podPhaseCounts,
podStatusCounts map[string]podStatusCounts,
) []inframonitoringtypes.StatefulSetRecord {
metricsMap := parseFullQueryResponse(resp, groupBy)
@@ -78,10 +77,6 @@ func buildStatefulSetRecords(
}
}
if podStatusCountsForGroup, ok := podStatusCounts[compositeKey]; ok {
record.PodCountsByStatus = podStatusCountsToResponse(podStatusCountsForGroup)
}
if attrs, ok := metadataMap[compositeKey]; ok {
for k, v := range attrs {
record.Meta[k] = v

View File

@@ -23,8 +23,6 @@ var statefulSetNameGroupByKey = qbtypes.GroupByKey{
// response to short-circuit cleanly when the phase metric is absent.
var statefulSetsTableMetricNamesList = []string{
"k8s.pod.phase",
"k8s.pod.status_reason",
"k8s.container.status.reason",
"k8s.pod.cpu.usage",
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",

View File

@@ -29,7 +29,7 @@ type module struct {
}
func NewModule(store llmpricingruletypes.Store, flagger flagger.Flagger, querier querier.Querier) llmpricingrule.Module {
return &module{store: store, flagger: flagger, querier: querier}
return &module{store: store, flagger: flagger}
}
func (module *module) List(ctx context.Context, orgID valuer.UUID, offset, limit int, search string, isOverride *bool) ([]*llmpricingruletypes.LLMPricingRule, int, error) {

View File

@@ -27,7 +27,6 @@ type ClusterRecord struct {
ClusterMemoryAllocatable float64 `json:"clusterMemoryAllocatable" required:"true"`
NodeCountsByReadiness NodeCountsByReadiness `json:"nodeCountsByReadiness" required:"true"`
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
Meta map[string]string `json:"meta" required:"true"`
}

View File

@@ -17,17 +17,16 @@ type DaemonSets struct {
}
type DaemonSetRecord struct {
DaemonSetName string `json:"daemonSetName" required:"true"`
DaemonSetCPU float64 `json:"daemonSetCPU" required:"true"`
DaemonSetCPURequest float64 `json:"daemonSetCPURequest" required:"true"`
DaemonSetCPULimit float64 `json:"daemonSetCPULimit" required:"true"`
DaemonSetMemory float64 `json:"daemonSetMemory" required:"true"`
DaemonSetMemoryRequest float64 `json:"daemonSetMemoryRequest" required:"true"`
DaemonSetMemoryLimit float64 `json:"daemonSetMemoryLimit" required:"true"`
DaemonSetName string `json:"daemonSetName" required:"true"`
DaemonSetCPU float64 `json:"daemonSetCPU" required:"true"`
DaemonSetCPURequest float64 `json:"daemonSetCPURequest" required:"true"`
DaemonSetCPULimit float64 `json:"daemonSetCPULimit" required:"true"`
DaemonSetMemory float64 `json:"daemonSetMemory" required:"true"`
DaemonSetMemoryRequest float64 `json:"daemonSetMemoryRequest" required:"true"`
DaemonSetMemoryLimit float64 `json:"daemonSetMemoryLimit" required:"true"`
DesiredNodes int `json:"desiredNodes" required:"true"`
CurrentNodes int `json:"currentNodes" required:"true"`
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
Meta map[string]string `json:"meta" required:"true"`
}

View File

@@ -27,7 +27,6 @@ type DeploymentRecord struct {
DesiredPods int `json:"desiredPods" required:"true"`
AvailablePods int `json:"availablePods" required:"true"`
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
Meta map[string]string `json:"meta" required:"true"`
}

View File

@@ -29,7 +29,6 @@ type JobRecord struct {
FailedPods int `json:"failedPods" required:"true"`
SuccessfulPods int `json:"successfulPods" required:"true"`
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
Meta map[string]string `json:"meta" required:"true"`
}

View File

@@ -17,12 +17,11 @@ type Namespaces struct {
}
type NamespaceRecord struct {
NamespaceName string `json:"namespaceName" required:"true"`
NamespaceCPU float64 `json:"namespaceCPU" required:"true"`
NamespaceMemory float64 `json:"namespaceMemory" required:"true"`
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
Meta map[string]string `json:"meta" required:"true"`
NamespaceName string `json:"namespaceName" required:"true"`
NamespaceCPU float64 `json:"namespaceCPU" required:"true"`
NamespaceMemory float64 `json:"namespaceMemory" required:"true"`
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
Meta map[string]string `json:"meta" required:"true"`
}
// PostableNamespaces is the request body for the v2 namespaces list API.

View File

@@ -28,7 +28,6 @@ type NodeRecord struct {
Condition NodeCondition `json:"condition" required:"true"`
NodeCountsByReadiness NodeCountsByReadiness `json:"nodeCountsByReadiness" required:"true"`
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
NodeCPU float64 `json:"nodeCPU" required:"true"`
NodeCPUAllocatable float64 `json:"nodeCPUAllocatable" required:"true"`
NodeMemory float64 `json:"nodeMemory" required:"true"`

View File

@@ -26,49 +26,18 @@ type PodCountsByPhase struct {
Unknown int `json:"unknown" required:"true"`
}
// PodCountsByStatus buckets pod counts by their latest kubectl-style display
// status in the time window (see PodStatus). One field per derivable status.
type PodCountsByStatus struct {
// Phase fallback.
Pending int `json:"pending" required:"true"`
Running int `json:"running" required:"true"`
Failed int `json:"failed" required:"true"`
Unknown int `json:"unknown" required:"true"`
// Container-level reasons.
CrashLoopBackOff int `json:"crashLoopBackOff" required:"true"`
ImagePullBackOff int `json:"imagePullBackOff" required:"true"`
ErrImagePull int `json:"errImagePull" required:"true"`
CreateContainerConfigError int `json:"createContainerConfigError" required:"true"`
ContainerCreating int `json:"containerCreating" required:"true"`
OOMKilled int `json:"oomKilled" required:"true"`
Completed int `json:"completed" required:"true"`
Error int `json:"error" required:"true"`
ContainerCannotRun int `json:"containerCannotRun" required:"true"`
// Pod-level reasons.
Evicted int `json:"evicted" required:"true"`
NodeAffinity int `json:"nodeAffinity" required:"true"`
NodeLost int `json:"nodeLost" required:"true"`
Shutdown int `json:"shutdown" required:"true"`
UnexpectedAdmissionError int `json:"unexpectedAdmissionError" required:"true"`
}
type PodRecord struct {
PodUID string `json:"podUID" required:"true"`
PodCPU float64 `json:"podCPU" required:"true"`
PodCPURequest float64 `json:"podCPURequest" required:"true"`
PodCPULimit float64 `json:"podCPULimit" required:"true"`
PodMemory float64 `json:"podMemory" required:"true"`
PodMemoryRequest float64 `json:"podMemoryRequest" required:"true"`
PodMemoryLimit float64 `json:"podMemoryLimit" required:"true"`
PodPhase PodPhase `json:"podPhase" required:"true"`
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
PodStatus PodStatus `json:"podStatus" required:"true"`
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
PodRestarts int64 `json:"podRestarts" required:"true"`
PodAge int64 `json:"podAge" required:"true"`
Meta map[string]string `json:"meta" required:"true"`
PodUID string `json:"podUID" required:"true"`
PodCPU float64 `json:"podCPU" required:"true"`
PodCPURequest float64 `json:"podCPURequest" required:"true"`
PodCPULimit float64 `json:"podCPULimit" required:"true"`
PodMemory float64 `json:"podMemory" required:"true"`
PodMemoryRequest float64 `json:"podMemoryRequest" required:"true"`
PodMemoryLimit float64 `json:"podMemoryLimit" required:"true"`
PodPhase PodPhase `json:"podPhase" required:"true"`
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
PodAge int64 `json:"podAge" required:"true"`
Meta map[string]string `json:"meta" required:"true"`
}
// PostablePods is the request body for the v2 pods list API.

View File

@@ -26,70 +26,6 @@ func (PodPhase) Enum() []any {
}
}
// PodStatus is the kubectl-style pod display status, derived from
// k8s.pod.phase + k8s.pod.status_reason + k8s.container.status.reason
// (priority cascade: container reason > pod-level reason > phase).
// Wire values are lowercased by valuer.NewString (e.g. "crashloopbackoff");
// the comment strings below are the kubectl spelling for readability. The
// query emits kubectl-style strings, so SQL output maps to these constants
// explicitly (not by raw string-wrapping).
type PodStatus struct {
valuer.String
}
var (
// Phase fallback.
PodStatusPending = PodStatus{valuer.NewString("Pending")}
PodStatusRunning = PodStatus{valuer.NewString("Running")}
PodStatusFailed = PodStatus{valuer.NewString("Failed")}
PodStatusUnknown = PodStatus{valuer.NewString("Unknown")}
// Container-level reasons (k8s.container.status.reason allowlist).
PodStatusCrashLoopBackOff = PodStatus{valuer.NewString("CrashLoopBackOff")}
PodStatusImagePullBackOff = PodStatus{valuer.NewString("ImagePullBackOff")}
PodStatusErrImagePull = PodStatus{valuer.NewString("ErrImagePull")}
PodStatusCreateContainerConfigError = PodStatus{valuer.NewString("CreateContainerConfigError")}
PodStatusContainerCreating = PodStatus{valuer.NewString("ContainerCreating")}
PodStatusOOMKilled = PodStatus{valuer.NewString("OOMKilled")}
PodStatusCompleted = PodStatus{valuer.NewString("Completed")}
PodStatusError = PodStatus{valuer.NewString("Error")}
PodStatusContainerCannotRun = PodStatus{valuer.NewString("ContainerCannotRun")}
// Pod-level reasons (k8s.pod.status_reason).
PodStatusEvicted = PodStatus{valuer.NewString("Evicted")}
PodStatusNodeAffinity = PodStatus{valuer.NewString("NodeAffinity")}
PodStatusNodeLost = PodStatus{valuer.NewString("NodeLost")}
PodStatusShutdown = PodStatus{valuer.NewString("Shutdown")}
PodStatusUnexpectedAdmissionError = PodStatus{valuer.NewString("UnexpectedAdmissionError")}
// Sentinel when status cannot be derived (metrics absent / not in list view).
PodStatusNoData = PodStatus{valuer.NewString("no_data")}
)
func (PodStatus) Enum() []any {
return []any{
PodStatusPending,
PodStatusRunning,
PodStatusFailed,
PodStatusUnknown,
PodStatusCrashLoopBackOff,
PodStatusImagePullBackOff,
PodStatusErrImagePull,
PodStatusCreateContainerConfigError,
PodStatusContainerCreating,
PodStatusOOMKilled,
PodStatusCompleted,
PodStatusError,
PodStatusContainerCannotRun,
PodStatusEvicted,
PodStatusNodeAffinity,
PodStatusNodeLost,
PodStatusShutdown,
PodStatusUnexpectedAdmissionError,
PodStatusNoData,
}
}
// Numeric pod phase values emitted by the k8s.pod.phase metric
// (source: OTel kubeletstats receiver).
const (

View File

@@ -27,7 +27,6 @@ type StatefulSetRecord struct {
DesiredPods int `json:"desiredPods" required:"true"`
CurrentPods int `json:"currentPods" required:"true"`
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
Meta map[string]string `json:"meta" required:"true"`
}

View File

@@ -1,52 +0,0 @@
"""Shared constants/helpers for v2 infra-monitoring pod-status tests."""
# All 18 PodCountsByStatus buckets (camelCase, matches inframonitoringtypes.PodCountsByStatus / the API response).
STATUS_BUCKETS = (
"pending",
"running",
"failed",
"unknown",
"crashLoopBackOff",
"imagePullBackOff",
"errImagePull",
"createContainerConfigError",
"containerCreating",
"oomKilled",
"completed",
"error",
"containerCannotRun",
"evicted",
"nodeAffinity",
"nodeLost",
"shutdown",
"unexpectedAdmissionError",
)
# Maps a PodStatus wire value (lowercase) to its PodCountsByStatus bucket key (camelCase).
STATUS_TO_BUCKET = {
"pending": "pending",
"running": "running",
"failed": "failed",
"unknown": "unknown",
"crashloopbackoff": "crashLoopBackOff",
"imagepullbackoff": "imagePullBackOff",
"errimagepull": "errImagePull",
"createcontainerconfigerror": "createContainerConfigError",
"containercreating": "containerCreating",
"oomkilled": "oomKilled",
"completed": "completed",
"error": "error",
"containercannotrun": "containerCannotRun",
"evicted": "evicted",
"nodeaffinity": "nodeAffinity",
"nodelost": "nodeLost",
"shutdown": "shutdown",
"unexpectedadmissionerror": "unexpectedAdmissionError",
}
def expected_status_counts(**nonzero: int) -> dict:
"""Full 19-bucket PodCountsByStatus with the given buckets set, rest 0."""
counts = {bucket: 0 for bucket in STATUS_BUCKETS}
counts.update(nonzero)
return counts

View File

@@ -34,30 +34,3 @@
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pp-fail-2-uid","k8s.pod.name":"pp-fail-2","k8s.namespace.name":"ns-x","k8s.node.name":"pp-node","k8s.cluster.name":"pp-cluster"},"timestamp":"2025-01-10T10:00:00+00:00","value":4,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pp-fail-2-uid","k8s.pod.name":"pp-fail-2","k8s.namespace.name":"ns-x","k8s.node.name":"pp-node","k8s.cluster.name":"pp-cluster"},"timestamp":"2025-01-10T10:02:00+00:00","value":4,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pp-fail-2-uid","k8s.pod.name":"pp-fail-2","k8s.namespace.name":"ns-x","k8s.node.name":"pp-node","k8s.cluster.name":"pp-cluster"},"timestamp":"2025-01-10T10:04:00+00:00","value":4,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-1-uid", "k8s.pod.name": "pp-run-1", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-1-uid", "k8s.pod.name": "pp-run-1", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-1-uid", "k8s.pod.name": "pp-run-1", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-2-uid", "k8s.pod.name": "pp-run-2", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "pp-run-2-uid", "k8s.pod.name": "pp-run-2", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-2-uid", "k8s.pod.name": "pp-run-2", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "pp-run-2-uid", "k8s.pod.name": "pp-run-2", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-2-uid", "k8s.pod.name": "pp-run-2", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "pp-run-2-uid", "k8s.pod.name": "pp-run-2", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-3-uid", "k8s.pod.name": "pp-run-3", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-3-uid", "k8s.pod.name": "pp-run-3", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-3-uid", "k8s.pod.name": "pp-run-3", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-4-uid", "k8s.pod.name": "pp-run-4", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-4-uid", "k8s.pod.name": "pp-run-4", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-4-uid", "k8s.pod.name": "pp-run-4", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-pend-1-uid", "k8s.pod.name": "pp-pend-1", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-pend-1-uid", "k8s.pod.name": "pp-pend-1", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-pend-1-uid", "k8s.pod.name": "pp-pend-1", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-fail-1-uid", "k8s.pod.name": "pp-fail-1", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "pp-fail-1-uid", "k8s.pod.name": "pp-fail-1", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster", "k8s.container.name": "app", "k8s.container.status.reason": "Error"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-fail-1-uid", "k8s.pod.name": "pp-fail-1", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "pp-fail-1-uid", "k8s.pod.name": "pp-fail-1", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster", "k8s.container.name": "app", "k8s.container.status.reason": "Error"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-fail-1-uid", "k8s.pod.name": "pp-fail-1", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "pp-fail-1-uid", "k8s.pod.name": "pp-fail-1", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster", "k8s.container.name": "app", "k8s.container.status.reason": "Error"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-fail-2-uid", "k8s.pod.name": "pp-fail-2", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-fail-2-uid", "k8s.pod.name": "pp-fail-2", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-fail-2-uid", "k8s.pod.name": "pp-fail-2", "k8s.namespace.name": "ns-x", "k8s.node.name": "pp-node", "k8s.cluster.name": "pp-cluster"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}

View File

@@ -151,30 +151,3 @@
{"metric_name":"k8s.deployment.available","labels":{"k8s.deployment.name":"pp-dep","k8s.namespace.name":"ns-pp","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":4,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.deployment.desired","labels":{"k8s.deployment.name":"pp-dep","k8s.namespace.name":"ns-pp","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":7,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.deployment.available","labels":{"k8s.deployment.name":"pp-dep","k8s.namespace.name":"ns-pp","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":4,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-1-uid", "k8s.pod.name": "pp-run-1", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-1-uid", "k8s.pod.name": "pp-run-1", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-1-uid", "k8s.pod.name": "pp-run-1", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-2-uid", "k8s.pod.name": "pp-run-2", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "pp-run-2-uid", "k8s.pod.name": "pp-run-2", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-2-uid", "k8s.pod.name": "pp-run-2", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "pp-run-2-uid", "k8s.pod.name": "pp-run-2", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-2-uid", "k8s.pod.name": "pp-run-2", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "pp-run-2-uid", "k8s.pod.name": "pp-run-2", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-3-uid", "k8s.pod.name": "pp-run-3", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-3-uid", "k8s.pod.name": "pp-run-3", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-3-uid", "k8s.pod.name": "pp-run-3", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-4-uid", "k8s.pod.name": "pp-run-4", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-4-uid", "k8s.pod.name": "pp-run-4", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-run-4-uid", "k8s.pod.name": "pp-run-4", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-pen-1-uid", "k8s.pod.name": "pp-pen-1", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-pen-1-uid", "k8s.pod.name": "pp-pen-1", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-pen-1-uid", "k8s.pod.name": "pp-pen-1", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-fail-1-uid", "k8s.pod.name": "pp-fail-1", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "pp-fail-1-uid", "k8s.pod.name": "pp-fail-1", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "Error"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-fail-1-uid", "k8s.pod.name": "pp-fail-1", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "pp-fail-1-uid", "k8s.pod.name": "pp-fail-1", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "Error"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-fail-1-uid", "k8s.pod.name": "pp-fail-1", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "pp-fail-1-uid", "k8s.pod.name": "pp-fail-1", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x", "k8s.container.name": "app", "k8s.container.status.reason": "Error"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-fail-2-uid", "k8s.pod.name": "pp-fail-2", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-fail-2-uid", "k8s.pod.name": "pp-fail-2", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pp-fail-2-uid", "k8s.pod.name": "pp-fail-2", "k8s.deployment.name": "pp-dep", "k8s.namespace.name": "ns-pp", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}

View File

@@ -103,45 +103,3 @@
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"unk-p-uid","k8s.pod.name":"unk-p","k8s.namespace.name":"ns-x","k8s.node.name":"node-a","k8s.deployment.name":"phase-test","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"","k8s.daemonset.name":"","k8s.job.name":"","k8s.cronjob.name":"","k8s.pod.start_time":"__START_TIME__"},"timestamp":"2025-01-10T10:00:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"unk-p-uid","k8s.pod.name":"unk-p","k8s.namespace.name":"ns-x","k8s.node.name":"node-a","k8s.deployment.name":"phase-test","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"","k8s.daemonset.name":"","k8s.job.name":"","k8s.cronjob.name":"","k8s.pod.start_time":"__START_TIME__"},"timestamp":"2025-01-10T10:02:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"unk-p-uid","k8s.pod.name":"unk-p","k8s.namespace.name":"ns-x","k8s.node.name":"node-a","k8s.deployment.name":"phase-test","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"","k8s.daemonset.name":"","k8s.job.name":"","k8s.cronjob.name":"","k8s.pod.start_time":"__START_TIME__"},"timestamp":"2025-01-10T10:04:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pend-p-uid", "k8s.pod.name": "pend-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pend-p-uid", "k8s.pod.name": "pend-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "pend-p-uid", "k8s.pod.name": "pend-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "run-p-uid", "k8s.pod.name": "run-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "run-p-uid", "k8s.pod.name": "run-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "run-p-uid", "k8s.pod.name": "run-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "run-p-uid", "k8s.pod.name": "run-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "run-p-uid", "k8s.pod.name": "run-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "run-p-uid", "k8s.pod.name": "run-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "run-p-uid", "k8s.pod.name": "run-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "run-p-uid", "k8s.pod.name": "run-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "run-p-uid", "k8s.pod.name": "run-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "succ-p-uid", "k8s.pod.name": "succ-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "succ-p-uid", "k8s.pod.name": "succ-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "succ-p-uid", "k8s.pod.name": "succ-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "succ-p-uid", "k8s.pod.name": "succ-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "Completed"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "succ-p-uid", "k8s.pod.name": "succ-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "Completed"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "succ-p-uid", "k8s.pod.name": "succ-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "Completed"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "succ-p-uid", "k8s.pod.name": "succ-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "succ-p-uid", "k8s.pod.name": "succ-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "succ-p-uid", "k8s.pod.name": "succ-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "fail-p-uid", "k8s.pod.name": "fail-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "fail-p-uid", "k8s.pod.name": "fail-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "fail-p-uid", "k8s.pod.name": "fail-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "fail-p-uid", "k8s.pod.name": "fail-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "fail-p-uid", "k8s.pod.name": "fail-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "fail-p-uid", "k8s.pod.name": "fail-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-oom", "k8s.container.status.reason": "OOMKilled"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-oom", "k8s.container.status.reason": "OOMKilled"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-oom", "k8s.container.status.reason": "OOMKilled"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-clbo", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-clbo", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-clbo", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-oom"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-oom"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-oom"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-clbo"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-clbo"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "unk-p-uid", "k8s.pod.name": "unk-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "c-clbo"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}

View File

@@ -124,42 +124,3 @@
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"g-pend-1-uid","k8s.pod.name":"g-pend-1","k8s.namespace.name":"ns-mixed","k8s.node.name":"node-b","k8s.deployment.name":"dep-y","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"","k8s.daemonset.name":"","k8s.job.name":"","k8s.cronjob.name":"","k8s.pod.start_time":"__START_TIME__"},"timestamp":"2025-01-10T10:00:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"g-pend-1-uid","k8s.pod.name":"g-pend-1","k8s.namespace.name":"ns-mixed","k8s.node.name":"node-b","k8s.deployment.name":"dep-y","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"","k8s.daemonset.name":"","k8s.job.name":"","k8s.cronjob.name":"","k8s.pod.start_time":"__START_TIME__"},"timestamp":"2025-01-10T10:02:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"g-pend-1-uid","k8s.pod.name":"g-pend-1","k8s.namespace.name":"ns-mixed","k8s.node.name":"node-b","k8s.deployment.name":"dep-y","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"","k8s.daemonset.name":"","k8s.job.name":"","k8s.cronjob.name":"","k8s.pod.start_time":"__START_TIME__"},"timestamp":"2025-01-10T10:04:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-run-1-uid", "k8s.pod.name": "g-run-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-run-1-uid", "k8s.pod.name": "g-run-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-run-1-uid", "k8s.pod.name": "g-run-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-run-1-uid", "k8s.pod.name": "g-run-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-run-1-uid", "k8s.pod.name": "g-run-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-run-1-uid", "k8s.pod.name": "g-run-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-run-3-uid", "k8s.pod.name": "g-run-3", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-run-3-uid", "k8s.pod.name": "g-run-3", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-run-3-uid", "k8s.pod.name": "g-run-3", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-run-3-uid", "k8s.pod.name": "g-run-3", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-run-3-uid", "k8s.pod.name": "g-run-3", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-run-3-uid", "k8s.pod.name": "g-run-3", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-run-2-uid", "k8s.pod.name": "g-run-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-run-2-uid", "k8s.pod.name": "g-run-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-run-2-uid", "k8s.pod.name": "g-run-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "g-run-2-uid", "k8s.pod.name": "g-run-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "g-run-2-uid", "k8s.pod.name": "g-run-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "g-run-2-uid", "k8s.pod.name": "g-run-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-run-2-uid", "k8s.pod.name": "g-run-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-run-2-uid", "k8s.pod.name": "g-run-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-run-2-uid", "k8s.pod.name": "g-run-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-fail-1-uid", "k8s.pod.name": "g-fail-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-fail-1-uid", "k8s.pod.name": "g-fail-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-fail-1-uid", "k8s.pod.name": "g-fail-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "g-fail-1-uid", "k8s.pod.name": "g-fail-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "Error"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "g-fail-1-uid", "k8s.pod.name": "g-fail-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "Error"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "g-fail-1-uid", "k8s.pod.name": "g-fail-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "Error"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-fail-1-uid", "k8s.pod.name": "g-fail-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-fail-1-uid", "k8s.pod.name": "g-fail-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-fail-1-uid", "k8s.pod.name": "g-fail-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-fail-2-uid", "k8s.pod.name": "g-fail-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-fail-2-uid", "k8s.pod.name": "g-fail-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-fail-2-uid", "k8s.pod.name": "g-fail-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-fail-2-uid", "k8s.pod.name": "g-fail-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-fail-2-uid", "k8s.pod.name": "g-fail-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "g-fail-2-uid", "k8s.pod.name": "g-fail-2", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-pend-1-uid", "k8s.pod.name": "g-pend-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-pend-1-uid", "k8s.pod.name": "g-pend-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "g-pend-1-uid", "k8s.pod.name": "g-pend-1", "k8s.namespace.name": "ns-mixed", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-y", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}

View File

@@ -19,10 +19,3 @@
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"trans-p-uid","k8s.pod.name":"trans-p","k8s.namespace.name":"ns-x","k8s.node.name":"node-a","k8s.deployment.name":"phase-test","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"","k8s.daemonset.name":"","k8s.job.name":"","k8s.cronjob.name":"","k8s.pod.start_time":"__START_TIME__"},"timestamp":"2025-01-10T10:00:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"trans-p-uid","k8s.pod.name":"trans-p","k8s.namespace.name":"ns-x","k8s.node.name":"node-a","k8s.deployment.name":"phase-test","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"","k8s.daemonset.name":"","k8s.job.name":"","k8s.cronjob.name":"","k8s.pod.start_time":"__START_TIME__"},"timestamp":"2025-01-10T10:02:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"trans-p-uid","k8s.pod.name":"trans-p","k8s.namespace.name":"ns-x","k8s.node.name":"node-a","k8s.deployment.name":"phase-test","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"","k8s.daemonset.name":"","k8s.job.name":"","k8s.cronjob.name":"","k8s.pod.start_time":"__START_TIME__"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "trans-p-uid", "k8s.pod.name": "trans-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "trans-p-uid", "k8s.pod.name": "trans-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "trans-p-uid", "k8s.pod.name": "trans-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "trans-p-uid", "k8s.pod.name": "trans-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff", "container.id": "aaa"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "trans-p-uid", "k8s.pod.name": "trans-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff", "container.id": "bbb"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "trans-p-uid", "k8s.pod.name": "trans-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "container.id": "aaa"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "trans-p-uid", "k8s.pod.name": "trans-p", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-a", "k8s.deployment.name": "phase-test", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "container.id": "bbb"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}

View File

@@ -40,21 +40,3 @@
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2-uid","k8s.pod.name":"acc-p2","k8s.namespace.name":"ns-b","k8s.node.name":"node-b","k8s.deployment.name":"dep-2","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"","k8s.daemonset.name":"","k8s.job.name":"","k8s.cronjob.name":"","k8s.pod.start_time":"__START_TIME__"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2-uid","k8s.pod.name":"acc-p2","k8s.namespace.name":"ns-b","k8s.node.name":"node-b","k8s.deployment.name":"dep-2","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"","k8s.daemonset.name":"","k8s.job.name":"","k8s.cronjob.name":"","k8s.pod.start_time":"__START_TIME__"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2-uid","k8s.pod.name":"acc-p2","k8s.namespace.name":"ns-b","k8s.node.name":"node-b","k8s.deployment.name":"dep-2","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"","k8s.daemonset.name":"","k8s.job.name":"","k8s.cronjob.name":"","k8s.pod.start_time":"__START_TIME__"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "acc-p1-uid", "k8s.pod.name": "acc-p1", "k8s.namespace.name": "ns-a", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-1", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "acc-p1-uid", "k8s.pod.name": "acc-p1", "k8s.namespace.name": "ns-a", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-1", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "acc-p1-uid", "k8s.pod.name": "acc-p1", "k8s.namespace.name": "ns-a", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-1", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "acc-p1-uid", "k8s.pod.name": "acc-p1", "k8s.namespace.name": "ns-a", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-1", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "acc-p1-uid", "k8s.pod.name": "acc-p1", "k8s.namespace.name": "ns-a", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-1", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "acc-p1-uid", "k8s.pod.name": "acc-p1", "k8s.namespace.name": "ns-a", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-1", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "acc-p1-uid", "k8s.pod.name": "acc-p1", "k8s.namespace.name": "ns-a", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-1", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "acc-p1-uid", "k8s.pod.name": "acc-p1", "k8s.namespace.name": "ns-a", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-1", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "acc-p1-uid", "k8s.pod.name": "acc-p1", "k8s.namespace.name": "ns-a", "k8s.node.name": "node-a", "k8s.deployment.name": "dep-1", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "acc-p2-uid", "k8s.pod.name": "acc-p2", "k8s.namespace.name": "ns-b", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-2", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "acc-p2-uid", "k8s.pod.name": "acc-p2", "k8s.namespace.name": "ns-b", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-2", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "acc-p2-uid", "k8s.pod.name": "acc-p2", "k8s.namespace.name": "ns-b", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-2", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "acc-p2-uid", "k8s.pod.name": "acc-p2", "k8s.namespace.name": "ns-b", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-2", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "acc-p2-uid", "k8s.pod.name": "acc-p2", "k8s.namespace.name": "ns-b", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-2", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.restarts", "labels": {"k8s.pod.uid": "acc-p2-uid", "k8s.pod.name": "acc-p2", "k8s.namespace.name": "ns-b", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-2", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "acc-p2-uid", "k8s.pod.name": "acc-p2", "k8s.namespace.name": "ns-b", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-2", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "acc-p2-uid", "k8s.pod.name": "acc-p2", "k8s.namespace.name": "ns-b", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-2", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.container.status.reason", "labels": {"k8s.pod.uid": "acc-p2-uid", "k8s.pod.name": "acc-p2", "k8s.namespace.name": "ns-b", "k8s.node.name": "node-b", "k8s.deployment.name": "dep-2", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "", "k8s.daemonset.name": "", "k8s.job.name": "", "k8s.cronjob.name": "", "k8s.pod.start_time": "__START_TIME__", "k8s.container.name": "app", "k8s.container.status.reason": "CrashLoopBackOff"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}

View File

@@ -9,17 +9,7 @@
"podMemoryRequest": 0.5,
"podMemoryLimit": 0.25,
"podPhase": "running",
"podCountsByPhase": {"pending": 0, "running": 1, "succeeded": 0, "failed": 0, "unknown": 0},
"podStatus": "running",
"podRestarts": 0,
"podCountsByStatus": {
"pending": 0, "running": 1, "failed": 0, "unknown": 0,
"crashLoopBackOff": 0, "imagePullBackOff": 0, "errImagePull": 0,
"createContainerConfigError": 0, "containerCreating": 0, "oomKilled": 0,
"completed": 0, "error": 0, "containerCannotRun": 0,
"evicted": 0, "nodeAffinity": 0, "nodeLost": 0, "shutdown": 0,
"unexpectedAdmissionError": 0
}
"podCountsByPhase": {"pending": 0, "running": 1, "succeeded": 0, "failed": 0, "unknown": 0}
},
{
"podName": "acc-p2",
@@ -30,17 +20,7 @@
"podMemoryRequest": 0.75,
"podMemoryLimit": 0.5,
"podPhase": "running",
"podCountsByPhase": {"pending": 0, "running": 1, "succeeded": 0, "failed": 0, "unknown": 0},
"podStatus": "running",
"podRestarts": 0,
"podCountsByStatus": {
"pending": 0, "running": 1, "failed": 0, "unknown": 0,
"crashLoopBackOff": 0, "imagePullBackOff": 0, "errImagePull": 0,
"createContainerConfigError": 0, "containerCreating": 0, "oomKilled": 0,
"completed": 0, "error": 0, "containerCannotRun": 0,
"evicted": 0, "nodeAffinity": 0, "nodeLost": 0, "shutdown": 0,
"unexpectedAdmissionError": 0
}
"podCountsByPhase": {"pending": 0, "running": 1, "succeeded": 0, "failed": 0, "unknown": 0}
}
]
}

View File

@@ -10,7 +10,6 @@ import requests
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.fs import get_testdata_file_path
from fixtures.inframonitoring import STATUS_BUCKETS, STATUS_TO_BUCKET
from fixtures.metrics import Metrics
from fixtures.querier import compare_values, get_all_warnings
from fixtures.time import parse_timestamp
@@ -122,9 +121,6 @@ def test_pods_accuracy(
"podMemoryLimit",
"podPhase",
"podCountsByPhase",
"podStatus",
"podCountsByStatus",
"podRestarts",
"podAge",
"meta",
):
@@ -135,11 +131,6 @@ def test_pods_accuracy(
assert bucket in record["podCountsByPhase"], f"missing phase bucket {bucket} in {record['podCountsByPhase']!r}"
assert isinstance(record["podCountsByPhase"][bucket], int)
# All status buckets always present, integer-typed.
for bucket in STATUS_BUCKETS:
assert bucket in record["podCountsByStatus"], f"missing status bucket {bucket} in {record['podCountsByStatus']!r}"
assert isinstance(record["podCountsByStatus"][bucket], int)
# Exact values.
pod_name = record["meta"]["k8s.pod.name"]
exp = exp_by_name[pod_name]
@@ -154,9 +145,6 @@ def test_pods_accuracy(
assert compare_values(record[field], exp[field], 1e-9), f"{pod_name}.{field}: got {record[field]}, expected {exp[field]}"
assert record["podPhase"] == exp["podPhase"]
assert record["podCountsByPhase"] == exp["podCountsByPhase"]
assert record["podStatus"] == exp["podStatus"], f"{pod_name}.podStatus: got {record['podStatus']}, expected {exp['podStatus']}"
assert record["podCountsByStatus"] == exp["podCountsByStatus"], f"{pod_name}.podCountsByStatus mismatch: got {record['podCountsByStatus']}"
assert record["podRestarts"] == exp["podRestarts"], f"{pod_name}.podRestarts: got {record['podRestarts']}, expected {exp['podRestarts']}"
assert record["podAge"] == expected_age_ms, f"{pod_name}.podAge: got {record['podAge']}, expected {expected_age_ms}"
@@ -805,343 +793,3 @@ def test_pods_validation_errors(
error = response.json()["error"]
assert error["code"] == "invalid_input"
assert err_substr.lower() in error["message"].lower(), f"expected substring {err_substr!r} not found in: {error['message']!r}"
@pytest.mark.parametrize(
"pod_name,expected_status",
[
# Expectations are what `kubectl get pods` STATUS would show for the
# seeded K8s state in pods_phases.jsonl (not the query internals).
pytest.param("pend-p", "pending", id="pending_phase_fallback"),
pytest.param("run-p", "crashloopbackoff", id="container_reason_beats_running_phase"),
pytest.param("succ-p", "completed", id="completed_terminated_reason"),
pytest.param("fail-p", "evicted", id="pod_reason_beats_phase"),
pytest.param("unk-p", "crashloopbackoff", id="multi_container_priority"),
],
)
def test_pods_status_list_mode(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
pod_name: str,
expected_status: str,
) -> None:
"""List mode: podStatus is the kubectl-style display status derived from
k8s.pod.phase + k8s.pod.status_reason + k8s.container.status.reason.
Seeded states -> what kubectl would print:
pend-p Pending (unscheduled; phase fallback, no container reason)
run-p CrashLoopBackOff (container crashlooping; phase still Running)
succ-p Completed (container terminated Completed; phase Succeeded)
fail-p Evicted (pod-level Status.Reason overrides phase)
unk-p CrashLoopBackOff (2 containers: OOMKilled + CrashLoopBackOff)
NOTE unk-p: a multi-container pod. kubectl picks the reason by container
order; we pick by reason priority (waiting>terminated), so CrashLoopBackOff
wins over OOMKilled. Documented divergence for multi-container pods — metrics
carry no container ordering.
"""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
_load_pods_metrics(
"inframonitoring/pods_phases.jsonl",
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
"filter": {"expression": f"k8s.pod.name = '{pod_name}'"},
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
assert data["total"] == 1
rec = data["records"][0]
assert rec["meta"]["k8s.pod.name"] == pod_name
assert rec["podStatus"] == expected_status
# List mode: pod is its own group -> exactly its status bucket is 1.
bucket = STATUS_TO_BUCKET[expected_status]
assert rec["podCountsByStatus"][bucket] == 1
for other in STATUS_BUCKETS:
if other != bucket:
assert rec["podCountsByStatus"][other] == 0, f"expected {other}=0 when status={expected_status}, got {rec['podCountsByStatus']}"
@pytest.mark.parametrize(
"pod_name,expected_restarts",
[
# Mirrors kubectl RESTARTS (sum across the pod's containers).
pytest.param("run-p", 5, id="single_container"),
pytest.param("succ-p", 0, id="zero_restarts"),
pytest.param("unk-p", 10, id="multi_container_sum"), # 2 + 8
pytest.param("pend-p", -1, id="no_series_sentinel"), # unscheduled -> no metric -> -1
],
)
def test_pods_restarts_list_mode(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
pod_name: str,
expected_restarts: int,
) -> None:
"""podRestarts is the sum of k8s.container.restarts across the pod's
containers (kubectl RESTARTS). pend-p never scheduled so emits no restart
series -> -1 no-data sentinel (kubectl would show 0)."""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
_load_pods_metrics(
"inframonitoring/pods_phases.jsonl",
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
"filter": {"expression": f"k8s.pod.name = '{pod_name}'"},
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
assert data["total"] == 1
rec = data["records"][0]
assert rec["meta"]["k8s.pod.name"] == pod_name
assert rec["podRestarts"] == expected_restarts
def test_pods_status_latest_wins(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
) -> None:
"""A stale container reason from an old incarnation must not win. trans-p's
first incarnation (container.id=aaa) reported CrashLoopBackOff, then it
recovered in a new incarnation (container.id=bbb, CrashLoopBackOff=0) while
phase went Running. kubectl shows Running; the frozen stale series is
ignored via argMax-by-latest-timestamp per (pod, container, reason)."""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
_load_pods_metrics(
"inframonitoring/pods_phases_transition.jsonl",
base_time=now - timedelta(minutes=8),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json={
"start": int((now - timedelta(minutes=10)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
assert data["total"] == 1
rec = data["records"][0]
assert rec["meta"]["k8s.pod.name"] == "trans-p"
assert rec["podStatus"] == "running"
def test_pods_restarts_latest_wins(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
) -> None:
"""restartCount is cumulative per container. Across incarnations
(container.id=aaa reported 1, then container.id=bbb reported 5) podRestarts
is the LATEST value 5 (kubectl RESTARTS), not the sum 6 — incarnations must
not be double-counted."""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
_load_pods_metrics(
"inframonitoring/pods_phases_transition.jsonl",
base_time=now - timedelta(minutes=8),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json={
"start": int((now - timedelta(minutes=10)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
assert data["total"] == 1
rec = data["records"][0]
assert rec["meta"]["k8s.pod.name"] == "trans-p"
assert rec["podRestarts"] == 5
def test_pods_status_grouped_mode(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
) -> None:
"""groupBy=[k8s.namespace.name] aggregates each pod's display status across
ns-mixed. podStatus is no-data (no single pod identifies the group). Seeded
states -> kubectl: g-run-1/g-run-3 Running, g-run-2 CrashLoopBackOff,
g-fail-1 Error, g-fail-2 Evicted, g-pend-1 Pending."""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
_load_pods_metrics(
"inframonitoring/pods_phases_grouped.jsonl",
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
"groupBy": [
{
"name": "k8s.namespace.name",
"fieldDataType": "string",
"fieldContext": "resource",
}
],
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
assert data["total"] == 1
rec = data["records"][0]
assert rec["meta"].get("k8s.namespace.name") == "ns-mixed"
assert rec["podStatus"] == "no_data"
expected_counts = {bucket: 0 for bucket in STATUS_BUCKETS}
expected_counts.update(
{
"running": 2,
"crashLoopBackOff": 1,
"error": 1,
"evicted": 1,
"pending": 1,
}
)
assert rec["podCountsByStatus"] == expected_counts
def test_pods_restarts_grouped_mode(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
) -> None:
"""Grouped podRestarts is the sum of restarts across all pods in the group.
In ns-mixed only g-run-2 has restarts (3); all others 0 -> group total 3."""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
_load_pods_metrics(
"inframonitoring/pods_phases_grouped.jsonl",
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
"groupBy": [
{
"name": "k8s.namespace.name",
"fieldDataType": "string",
"fieldContext": "resource",
}
],
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
assert data["total"] == 1
rec = data["records"][0]
assert rec["meta"].get("k8s.namespace.name") == "ns-mixed"
assert rec["podRestarts"] == 3
def test_pods_status_missing_metric_warning(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
) -> None:
"""When the status metrics were never ingested, the status query is gated
off: a warning naming the missing metric(s) is surfaced, podStatus is the
no-data sentinel, and all status buckets are 0. (pods_missing_metrics.jsonl
seeds only k8s.pod.cpu.usage.)"""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
_load_pods_metrics(
"inframonitoring/pods_missing_metrics.jsonl",
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
"filter": {"expression": "k8s.pod.name = 'miss-p1'"},
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
body = response.json()
data = body["data"]
# Collect primary + additional warning messages.
warning = data.get("warning") or {}
msgs = ([warning["message"]] if warning.get("message") else []) + [w["message"] for w in warning.get("warnings", [])]
assert any("Pod status could not be computed" in m for m in msgs), f"status-gate warning missing: {msgs!r}"
assert any("k8s.container.status.reason" in m for m in msgs), f"missing metric not named: {msgs!r}"
rec = data["records"][0]
assert rec["meta"]["k8s.pod.name"] == "miss-p1"
assert rec["podStatus"] == "no_data"
for bucket in STATUS_BUCKETS:
assert rec["podCountsByStatus"][bucket] == 0, f"expected {bucket}=0 when gated off, got {rec['podCountsByStatus']}"
assert rec["podRestarts"] == -1

View File

@@ -10,7 +10,6 @@ import requests
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.fs import get_testdata_file_path
from fixtures.inframonitoring import expected_status_counts
from fixtures.metrics import Metrics
from fixtures.querier import compare_values, get_all_warnings
@@ -405,49 +404,6 @@ def test_clusters_pod_phase_aggregation(
}
def test_clusters_pod_status_aggregation(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
) -> None:
"""Cluster's pods aggregated by kubectl-style display status. Seeded states
in clusters_pod_phases.jsonl -> what kubectl would show:
pp-run-1/3/4 Running, pp-run-2 CrashLoopBackOff (phase Running),
pp-fail-1 Error, pp-fail-2 Evicted (pod-level), pp-pend-1 Pending.
"""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
Metrics.load_from_file(
get_testdata_file_path("inframonitoring/clusters_pod_phases.jsonl"),
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
"filter": {"expression": "k8s.cluster.name = 'pp-cluster'"},
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
assert data["total"] == 1
rec = data["records"][0]
assert rec["clusterName"] == "pp-cluster"
assert rec["podCountsByStatus"] == expected_status_counts(running=3, crashLoopBackOff=1, error=1, evicted=1, pending=1)
# Phase counts unchanged by the status enrichment.
assert rec["podCountsByPhase"] == {"pending": 1, "running": 4, "succeeded": 0, "failed": 2, "unknown": 0}
# All status metrics present -> gate satisfied -> no status warning.
assert all("Pod status could not be computed" not in w["message"] for w in get_all_warnings(response.json()))
@pytest.mark.parametrize(
"group_key,expected",
[

View File

@@ -10,7 +10,6 @@ import requests
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.fs import get_testdata_file_path
from fixtures.inframonitoring import expected_status_counts
from fixtures.metrics import Metrics
from fixtures.querier import compare_values, get_all_warnings
@@ -386,49 +385,6 @@ def test_deployments_pod_phase_aggregation(
}
def test_deployments_pod_status_aggregation(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
) -> None:
"""Deployment's pods aggregated by kubectl-style display status. Seeded states
in deployments_pod_phases.jsonl -> what kubectl would show:
pp-run-1/3/4 Running, pp-run-2 CrashLoopBackOff (phase Running),
pp-fail-1 Error, pp-fail-2 Evicted (pod-level), pp-pen-1 Pending.
"""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
Metrics.load_from_file(
get_testdata_file_path("inframonitoring/deployments_pod_phases.jsonl"),
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
"filter": {"expression": "k8s.deployment.name = 'pp-dep'"},
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
assert data["total"] == 1
rec = data["records"][0]
assert rec["deploymentName"] == "pp-dep"
assert rec["podCountsByStatus"] == expected_status_counts(running=3, crashLoopBackOff=1, error=1, evicted=1, pending=1)
# Phase counts unchanged by the status enrichment.
assert rec["podCountsByPhase"] == {"pending": 1, "running": 4, "succeeded": 0, "failed": 2, "unknown": 0}
# All status metrics present -> gate satisfied -> no status warning.
assert all("Pod status could not be computed" not in w["message"] for w in get_all_warnings(response.json()))
def test_deployments_desired_available_counts(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument

View File

@@ -45,13 +45,6 @@ _PODS_OPT = [
"k8s.pod.memory_limit_utilization",
]
# Default-off pod-status metrics (k8sclusterreceiver), optional on every tab
# that surfaces pod status counts.
_POD_STATUS_OPT = [
"k8s.pod.status_reason",
"k8s.container.status.reason",
]
# Mirror of checkSpecs: type -> {default|optional: {component: [metrics]}, attrs: {component: [attrs]}}.
SPECS = {
"hosts": {
@@ -65,8 +58,8 @@ SPECS = {
"attrs": {HMR: ["process.pid"]},
},
"pods": {
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase", "k8s.container.restarts"]},
"optional": {KSR: list(_PODS_OPT), KCR: ["k8s.pod.status_reason", "k8s.container.status.reason"]},
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase"]},
"optional": {KSR: list(_PODS_OPT)},
"attrs": {KAP: ["k8s.pod.uid"]},
},
"nodes": {
@@ -74,37 +67,37 @@ SPECS = {
KSR: ["k8s.node.cpu.usage", "k8s.node.memory.working_set"],
KCR: ["k8s.node.allocatable_cpu", "k8s.node.allocatable_memory", "k8s.node.condition_ready", "k8s.pod.phase"],
},
"optional": {KCR: list(_POD_STATUS_OPT)},
"optional": {},
"attrs": {KAP: ["k8s.node.name"]},
},
"deployments": {
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase", "k8s.deployment.desired", "k8s.deployment.available"]},
"optional": {KSR: list(_PODS_OPT), KCR: list(_POD_STATUS_OPT)},
"optional": {KSR: list(_PODS_OPT)},
"attrs": {KAP: ["k8s.deployment.name", "k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
},
"daemonsets": {
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase", "k8s.daemonset.desired_scheduled_nodes", "k8s.daemonset.current_scheduled_nodes"]},
"optional": {KSR: list(_PODS_OPT), KCR: list(_POD_STATUS_OPT)},
"optional": {KSR: list(_PODS_OPT)},
"attrs": {KAP: ["k8s.daemonset.name", "k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
},
"statefulsets": {
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase", "k8s.statefulset.desired_pods", "k8s.statefulset.current_pods"]},
"optional": {KSR: list(_PODS_OPT), KCR: list(_POD_STATUS_OPT)},
"optional": {KSR: list(_PODS_OPT)},
"attrs": {KAP: ["k8s.statefulset.name", "k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
},
"jobs": {
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase", "k8s.job.desired_successful_pods", "k8s.job.active_pods", "k8s.job.failed_pods", "k8s.job.successful_pods"]},
"optional": {KSR: list(_PODS_OPT), KCR: list(_POD_STATUS_OPT)},
"optional": {KSR: list(_PODS_OPT)},
"attrs": {KAP: ["k8s.job.name", "k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
},
"namespaces": {
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase"]},
"optional": {KCR: list(_POD_STATUS_OPT)},
"optional": {},
"attrs": {KAP: ["k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
},
"clusters": {
"default": {KSR: ["k8s.node.cpu.usage", "k8s.node.memory.working_set"], KCR: ["k8s.node.allocatable_cpu", "k8s.node.allocatable_memory", "k8s.node.condition_ready", "k8s.pod.phase"]},
"optional": {KCR: list(_POD_STATUS_OPT)},
"optional": {},
"attrs": {RDP: ["k8s.cluster.name"]},
},
"volumes": {