mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-07 15:10:37 +01:00
Compare commits
56 Commits
fix/query-
...
infraM/get
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e1b2b3f49 | ||
|
|
f0615bc502 | ||
|
|
4d10f8e5ef | ||
|
|
e4776cddc4 | ||
|
|
c501f9d00d | ||
|
|
0869dfdb0b | ||
|
|
de2212e8b9 | ||
|
|
daf48f2321 | ||
|
|
ff4f18985c | ||
|
|
9074af81dd | ||
|
|
b12d37f2cc | ||
|
|
7d555b643a | ||
|
|
7839e806fd | ||
|
|
2dcd51391d | ||
|
|
6251616d9d | ||
|
|
652a9044f4 | ||
|
|
d4646657ca | ||
|
|
0d453f3c27 | ||
|
|
29681572db | ||
|
|
a3fa475e88 | ||
|
|
64d0ada495 | ||
|
|
60886fd906 | ||
|
|
d8954da307 | ||
|
|
7476a2cacb | ||
|
|
dc9c127925 | ||
|
|
612ffd8cf7 | ||
|
|
babb2e2cd1 | ||
|
|
0bd0f7ada9 | ||
|
|
66b40b5509 | ||
|
|
77e56eea8c | ||
|
|
c4448c9940 | ||
|
|
60c33003b8 | ||
|
|
a126cee885 | ||
|
|
916466d7d1 | ||
|
|
11fa6bbb10 | ||
|
|
b1136348c4 | ||
|
|
3bdff5a083 | ||
|
|
76eccec91c | ||
|
|
4d94a4090d | ||
|
|
406f676e7c | ||
|
|
73c267e3f7 | ||
|
|
e7fbf89d4a | ||
|
|
f08cea5491 | ||
|
|
79160910d1 | ||
|
|
fdcdd935a7 | ||
|
|
2c4a661edb | ||
|
|
20ddd8df8c | ||
|
|
744c5cf587 | ||
|
|
d4ddfceba5 | ||
|
|
c752131fe0 | ||
|
|
223c2a6c31 | ||
|
|
6bf20c5e93 | ||
|
|
09fce10c1d | ||
|
|
8e6891cf58 | ||
|
|
49019f44b6 | ||
|
|
075611fc6c |
@@ -155,8 +155,8 @@ querier:
|
||||
cache_ttl: 168h
|
||||
# The interval for recent data that should not be cached.
|
||||
flux_interval: 5m
|
||||
# The maximum number of concurrent queries for missing ranges.
|
||||
max_concurrent_queries: 4
|
||||
# The maximum number of queries a single query range request runs at once.
|
||||
max_concurrent_queries: 8
|
||||
# When filtering logs by trace_id, clamp the query window to the trace time
|
||||
# range with padding to include slightly delayed log exports. Logs only; set
|
||||
# to 0 to disable.
|
||||
|
||||
@@ -4252,6 +4252,34 @@ components:
|
||||
type: number
|
||||
clusterName:
|
||||
type: string
|
||||
counts:
|
||||
properties:
|
||||
daemonSets:
|
||||
format: int64
|
||||
type: integer
|
||||
deployments:
|
||||
format: int64
|
||||
type: integer
|
||||
jobs:
|
||||
format: int64
|
||||
type: integer
|
||||
namespaces:
|
||||
format: int64
|
||||
type: integer
|
||||
nodes:
|
||||
format: int64
|
||||
type: integer
|
||||
statefulSets:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- nodes
|
||||
- namespaces
|
||||
- deployments
|
||||
- daemonSets
|
||||
- jobs
|
||||
- statefulSets
|
||||
type: object
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
@@ -4261,6 +4289,8 @@ components:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeCountsByReadiness'
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
required:
|
||||
- clusterName
|
||||
- clusterCPU
|
||||
@@ -4269,6 +4299,8 @@ components:
|
||||
- clusterMemoryAllocatable
|
||||
- nodeCountsByReadiness
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- counts
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesClusters:
|
||||
@@ -4324,6 +4356,8 @@ components:
|
||||
type: object
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
required:
|
||||
- daemonSetName
|
||||
- daemonSetCPU
|
||||
@@ -4335,6 +4369,7 @@ components:
|
||||
- desiredNodes
|
||||
- currentNodes
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesDaemonSets:
|
||||
@@ -4390,6 +4425,8 @@ components:
|
||||
type: object
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
required:
|
||||
- deploymentName
|
||||
- deploymentCPU
|
||||
@@ -4401,6 +4438,7 @@ components:
|
||||
- desiredPods
|
||||
- availablePods
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesDeployments:
|
||||
@@ -4533,6 +4571,8 @@ components:
|
||||
type: object
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
successfulPods:
|
||||
type: integer
|
||||
required:
|
||||
@@ -4548,6 +4588,7 @@ components:
|
||||
- failedPods
|
||||
- successfulPods
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesJobs:
|
||||
@@ -4623,6 +4664,26 @@ components:
|
||||
type: object
|
||||
InframonitoringtypesNamespaceRecord:
|
||||
properties:
|
||||
counts:
|
||||
properties:
|
||||
daemonSets:
|
||||
format: int64
|
||||
type: integer
|
||||
deployments:
|
||||
format: int64
|
||||
type: integer
|
||||
jobs:
|
||||
format: int64
|
||||
type: integer
|
||||
statefulSets:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- deployments
|
||||
- daemonSets
|
||||
- jobs
|
||||
- statefulSets
|
||||
type: object
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
@@ -4638,11 +4699,15 @@ components:
|
||||
type: string
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
required:
|
||||
- namespaceName
|
||||
- namespaceCPU
|
||||
- namespaceMemory
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- counts
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesNamespaces:
|
||||
@@ -4708,11 +4773,14 @@ components:
|
||||
type: string
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
required:
|
||||
- nodeName
|
||||
- condition
|
||||
- nodeCountsByReadiness
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- nodeCPU
|
||||
- nodeCPUAllocatable
|
||||
- nodeMemory
|
||||
@@ -4758,6 +4826,64 @@ 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
|
||||
@@ -4788,6 +4914,8 @@ components:
|
||||
type: number
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
podMemory:
|
||||
format: double
|
||||
type: number
|
||||
@@ -4799,6 +4927,11 @@ components:
|
||||
type: number
|
||||
podPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodPhase'
|
||||
podRestarts:
|
||||
format: int64
|
||||
type: integer
|
||||
podStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
podUID:
|
||||
type: string
|
||||
required:
|
||||
@@ -4811,9 +4944,34 @@ 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:
|
||||
@@ -5112,6 +5270,8 @@ components:
|
||||
type: object
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
statefulSetCPU:
|
||||
format: double
|
||||
type: number
|
||||
@@ -5143,6 +5303,7 @@ components:
|
||||
- desiredPods
|
||||
- currentPods
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesStatefulSets:
|
||||
|
||||
149
docs/contributing/go/authz.md
Normal file
149
docs/contributing/go/authz.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# 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.
|
||||
@@ -11,6 +11,7 @@ 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
|
||||
|
||||
@@ -5660,6 +5660,39 @@ export interface InframonitoringtypesChecksDTO {
|
||||
type: InframonitoringtypesCheckTypeDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesClusterRecordDTOCounts = {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
daemonSets: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
deployments: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
jobs: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
namespaces: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
nodes: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
statefulSets: number;
|
||||
};
|
||||
|
||||
export type InframonitoringtypesClusterRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -5704,6 +5737,81 @@ 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
|
||||
@@ -5729,12 +5837,17 @@ export interface InframonitoringtypesClusterRecordDTO {
|
||||
* @type string
|
||||
*/
|
||||
clusterName: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
counts: InframonitoringtypesClusterRecordDTOCounts;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesClusterRecordDTOMeta;
|
||||
nodeCountsByReadiness: InframonitoringtypesNodeCountsByReadinessDTO;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesResponseTypeDTO {
|
||||
@@ -5838,6 +5951,7 @@ export interface InframonitoringtypesDaemonSetRecordDTO {
|
||||
*/
|
||||
meta: InframonitoringtypesDaemonSetRecordDTOMeta;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDaemonSetsDTO {
|
||||
@@ -5915,6 +6029,7 @@ export interface InframonitoringtypesDeploymentRecordDTO {
|
||||
*/
|
||||
meta: InframonitoringtypesDeploymentRecordDTOMeta;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDeploymentsDTO {
|
||||
@@ -6081,6 +6196,7 @@ export interface InframonitoringtypesJobRecordDTO {
|
||||
*/
|
||||
meta: InframonitoringtypesJobRecordDTOMeta;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -6104,6 +6220,29 @@ export interface InframonitoringtypesJobsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesNamespaceRecordDTOCounts = {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
daemonSets: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
deployments: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
jobs: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
statefulSets: number;
|
||||
};
|
||||
|
||||
export type InframonitoringtypesNamespaceRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6115,6 +6254,10 @@ export type InframonitoringtypesNamespaceRecordDTOMeta =
|
||||
InframonitoringtypesNamespaceRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesNamespaceRecordDTO {
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
counts: InframonitoringtypesNamespaceRecordDTOCounts;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
@@ -6134,6 +6277,7 @@ export interface InframonitoringtypesNamespaceRecordDTO {
|
||||
*/
|
||||
namespaceName: string;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesNamespacesDTO {
|
||||
@@ -6200,6 +6344,7 @@ export interface InframonitoringtypesNodeRecordDTO {
|
||||
*/
|
||||
nodeName: string;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesNodesDTO {
|
||||
@@ -6237,6 +6382,27 @@ 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
|
||||
@@ -6263,6 +6429,7 @@ export interface InframonitoringtypesPodRecordDTO {
|
||||
*/
|
||||
podCPURequest: number;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
@@ -6279,6 +6446,12 @@ export interface InframonitoringtypesPodRecordDTO {
|
||||
*/
|
||||
podMemoryRequest: number;
|
||||
podPhase: InframonitoringtypesPodPhaseDTO;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
podRestarts: number;
|
||||
podStatus: InframonitoringtypesPodStatusDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6596,6 +6769,7 @@ export interface InframonitoringtypesStatefulSetRecordDTO {
|
||||
*/
|
||||
meta: InframonitoringtypesStatefulSetRecordDTOMeta;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
|
||||
1
frontend/src/assets/Logos/cassandra.svg
Normal file
1
frontend/src/assets/Logos/cassandra.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
1
frontend/src/assets/Logos/fluxcd.svg
Normal file
1
frontend/src/assets/Logos/fluxcd.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
1
frontend/src/assets/Logos/planetscale.svg
Normal file
1
frontend/src/assets/Logos/planetscale.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 326 B |
@@ -2,6 +2,7 @@ import { Button } from 'antd';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
|
||||
interface CheckboxValueRowProps {
|
||||
value: string;
|
||||
@@ -46,9 +47,11 @@ function CheckboxValueRow({
|
||||
{customRendererForValue ? (
|
||||
customRendererForValue(value)
|
||||
) : (
|
||||
<Typography.Text className="value-string" truncate={1}>
|
||||
{String(value)}
|
||||
</Typography.Text>
|
||||
<TooltipSimple title={value} side="top" align="center" arrow>
|
||||
<Typography.Text className="value-string" truncate={1}>
|
||||
{String(value)}
|
||||
</Typography.Text>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
<Button type="text" className="only-btn">
|
||||
{onlyButtonLabel}
|
||||
|
||||
@@ -31,6 +31,7 @@ export enum LOCALSTORAGE {
|
||||
METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS',
|
||||
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
|
||||
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
||||
QUICK_FILTERS_WIDTH_LOGS = 'QUICK_FILTERS_WIDTH_LOGS',
|
||||
FUNNEL_STEPS = 'FUNNEL_STEPS',
|
||||
SPAN_DETAILS_PINNED_ATTRIBUTES = 'SPAN_DETAILS_PINNED_ATTRIBUTES',
|
||||
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',
|
||||
|
||||
@@ -177,7 +177,13 @@ export default function ConversationView({
|
||||
conversationId={conversationId}
|
||||
messages={messages}
|
||||
isStreaming={isStreamingHere}
|
||||
onSendSuggestedPrompt={(text): void => handleSend(text)}
|
||||
onSendSuggestedPrompt={(text): void => {
|
||||
handleSend(
|
||||
text,
|
||||
undefined,
|
||||
autoContexts.length > 0 ? autoContexts : undefined,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{showDisclaimer && (
|
||||
<div className={disclaimerClass} role="note" aria-live="polite">
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -149,6 +149,18 @@
|
||||
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 {
|
||||
|
||||
@@ -43,6 +43,7 @@ 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';
|
||||
|
||||
@@ -515,7 +516,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
</div>
|
||||
)}
|
||||
{!isEmpty(description) && (
|
||||
<section className="dashboard-description-section">{description}</section>
|
||||
<section className="dashboard-description-section">
|
||||
{linkifyText(description ?? '')}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{!isEmpty(dashboardVariables) && (
|
||||
|
||||
@@ -1625,6 +1625,9 @@ export const getHostQueryPayload = (
|
||||
const diskPendingKey = dotMetricsEnabled
|
||||
? 'system.disk.pending_operations'
|
||||
: 'system_disk_pending_operations';
|
||||
const fsUsageKey = dotMetricsEnabled
|
||||
? 'system.filesystem.usage'
|
||||
: 'system_filesystem_usage';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -2657,6 +2660,155 @@ 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,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -2732,4 +2884,5 @@ 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' },
|
||||
];
|
||||
|
||||
@@ -20,6 +20,7 @@ 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';
|
||||
@@ -51,6 +52,7 @@ 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';
|
||||
@@ -94,6 +96,7 @@ 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';
|
||||
@@ -120,6 +123,7 @@ 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';
|
||||
@@ -1526,6 +1530,28 @@ 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',
|
||||
@@ -1586,6 +1612,119 @@ 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',
|
||||
|
||||
@@ -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 StrictMode double-mount and
|
||||
// aborted navigations.
|
||||
// Router state (vs a module flag) survives page remounts and aborted
|
||||
// navigations.
|
||||
const fromInApp = location.state?.fromInApp === true;
|
||||
useEffect(() => {
|
||||
if (fromInApp) {
|
||||
@@ -52,18 +52,34 @@ export default function AIAssistantPage(): JSX.Element {
|
||||
(s) => s.startNewConversation,
|
||||
);
|
||||
|
||||
// Keep a ref so the effect can read latest conversations without re-firing
|
||||
// when startNewConversation mutates the store mid-effect.
|
||||
// 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).
|
||||
const conversationsRef = useRef(conversations);
|
||||
conversationsRef.current = conversations;
|
||||
const activeConversationIdRef = useRef(activeConversationId);
|
||||
activeConversationIdRef.current = activeConversationId;
|
||||
|
||||
useEffect(() => {
|
||||
if (conversationsRef.current[conversationId]) {
|
||||
// URL points at a known conversation → just activate it.
|
||||
if (conversationId && conversationsRef.current[conversationId]) {
|
||||
setActiveConversation(conversationId);
|
||||
} else {
|
||||
const newId = startNewConversation();
|
||||
history.replace(ROUTES.AI_ASSISTANT.replace(':conversationId', newId));
|
||||
return;
|
||||
}
|
||||
|
||||
// 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]);
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -32,6 +32,22 @@
|
||||
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;
|
||||
|
||||
@@ -14,6 +14,7 @@ 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';
|
||||
@@ -143,7 +144,14 @@ function DashboardInfo({
|
||||
)}
|
||||
|
||||
{hasDescription && (
|
||||
<TooltipSimple title={description} disableHoverableContent>
|
||||
<TooltipSimple
|
||||
side="bottom"
|
||||
title={
|
||||
<span className={styles.descriptionTooltip}>
|
||||
{linkifyText(description)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<SolidInfoCircle
|
||||
className={styles.descriptionIcon}
|
||||
size={14}
|
||||
|
||||
@@ -48,6 +48,13 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.footerStatus {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.validation {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
@@ -57,6 +64,32 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.danglingWarning {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--bg-amber-400);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.warningIcon {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.warningText {
|
||||
overflow: hidden;
|
||||
color: var(--bg-amber-400);
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// Above the JSON drawer (antd Drawer sits at ~1100) so the id list is visible.
|
||||
.warningTooltip {
|
||||
z-index: 1101;
|
||||
}
|
||||
|
||||
.validationValid {
|
||||
color: var(--bg-forest-400);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { KeyboardEvent, useCallback } from 'react';
|
||||
import MEditor from '@monaco-editor/react';
|
||||
import { TriangleAlert } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { Drawer } from 'antd';
|
||||
@@ -26,8 +28,18 @@ function JsonEditorDrawer({
|
||||
}: JsonEditorDrawerProps): JSX.Element {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const { draft, setDraft, validity, isDirty, isSaving, format, reset, apply } =
|
||||
useJsonEditor({ dashboard, isOpen, onApplied: onClose });
|
||||
const {
|
||||
draft,
|
||||
setDraft,
|
||||
validity,
|
||||
isDirty,
|
||||
isSaving,
|
||||
danglingPanelIds,
|
||||
missingPanelRefs,
|
||||
format,
|
||||
reset,
|
||||
apply,
|
||||
} = useJsonEditor({ dashboard, isOpen, onApplied: onClose });
|
||||
|
||||
const onCopy = useCallback((): void => {
|
||||
copyToClipboard(draft);
|
||||
@@ -48,6 +60,7 @@ function JsonEditorDrawer({
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLDivElement>): void => {
|
||||
event.stopPropagation();
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
void apply();
|
||||
@@ -60,6 +73,19 @@ function JsonEditorDrawer({
|
||||
const validationText = validity.valid
|
||||
? `Valid JSON · ${validity.lineCount} lines`
|
||||
: `Line ${validity.errorLine ?? '?'} · ${validity.message ?? 'Invalid JSON'}`;
|
||||
const plural = (n: number): string => (n === 1 ? '' : 's');
|
||||
const danglingWarning =
|
||||
danglingPanelIds.length > 0
|
||||
? `${danglingPanelIds.length} panel${plural(
|
||||
danglingPanelIds.length,
|
||||
)} not present in layout — they won't be shown after saving.`
|
||||
: null;
|
||||
const missingRefWarning =
|
||||
missingPanelRefs.length > 0
|
||||
? `${missingPanelRefs.length} layout item${plural(
|
||||
missingPanelRefs.length,
|
||||
)} ${missingPanelRefs.length === 1 ? 'references' : 'reference'} a panel that no longer exists.`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
@@ -71,15 +97,49 @@ function JsonEditorDrawer({
|
||||
rootClassName={styles.root}
|
||||
footer={
|
||||
<div className={styles.footer}>
|
||||
<Typography.Text
|
||||
className={cx(styles.validation, {
|
||||
[styles.validationValid]: validity.valid,
|
||||
[styles.validationInvalid]: !validity.valid,
|
||||
})}
|
||||
data-testid="json-editor-validation"
|
||||
>
|
||||
{validationText}
|
||||
</Typography.Text>
|
||||
<div className={styles.footerStatus}>
|
||||
<Typography.Text
|
||||
className={cx(styles.validation, {
|
||||
[styles.validationValid]: validity.valid,
|
||||
[styles.validationInvalid]: !validity.valid,
|
||||
})}
|
||||
data-testid="json-editor-validation"
|
||||
>
|
||||
{validationText}
|
||||
</Typography.Text>
|
||||
{danglingWarning && (
|
||||
<TooltipSimple
|
||||
title={danglingPanelIds.join(', ')}
|
||||
tooltipContentProps={{ className: styles.warningTooltip }}
|
||||
>
|
||||
<span
|
||||
className={styles.danglingWarning}
|
||||
data-testid="json-editor-dangling-warning"
|
||||
>
|
||||
<TriangleAlert size={12} className={styles.warningIcon} />
|
||||
<Typography.Text className={styles.warningText}>
|
||||
{danglingWarning}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
{missingRefWarning && (
|
||||
<TooltipSimple
|
||||
title={missingPanelRefs.join(', ')}
|
||||
tooltipContentProps={{ className: styles.warningTooltip }}
|
||||
>
|
||||
<span
|
||||
className={styles.danglingWarning}
|
||||
data-testid="json-editor-missing-ref-warning"
|
||||
>
|
||||
<TriangleAlert size={12} className={styles.warningIcon} />
|
||||
<Typography.Text className={styles.warningText}>
|
||||
{missingRefWarning}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.footerActions}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import JsonEditorDrawer from '../JsonEditorDrawer';
|
||||
@@ -48,11 +49,13 @@ function hookValue(
|
||||
validity: { valid: true, lineCount: 3 },
|
||||
isDirty: true,
|
||||
isSaving: false,
|
||||
danglingPanelIds: [],
|
||||
missingPanelRefs: [],
|
||||
format: jest.fn(),
|
||||
reset: jest.fn(),
|
||||
apply: jest.fn().mockResolvedValue(undefined),
|
||||
...overrides,
|
||||
};
|
||||
} as ReturnType<typeof useJsonEditor>;
|
||||
}
|
||||
|
||||
describe('JsonEditorDrawer', () => {
|
||||
@@ -81,6 +84,42 @@ describe('JsonEditorDrawer', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('warns about dangling panels, and hides the warning when there are none', () => {
|
||||
mockUseJsonEditor.mockReturnValue(
|
||||
hookValue({ danglingPanelIds: ['p1', 'p2'] }),
|
||||
);
|
||||
const { rerender } = render(
|
||||
<TooltipProvider>
|
||||
<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />
|
||||
</TooltipProvider>,
|
||||
);
|
||||
expect(screen.getByTestId('json-editor-dangling-warning')).toHaveTextContent(
|
||||
'2 panels not present in layout',
|
||||
);
|
||||
|
||||
mockUseJsonEditor.mockReturnValue(hookValue({ danglingPanelIds: [] }));
|
||||
rerender(
|
||||
<TooltipProvider>
|
||||
<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />
|
||||
</TooltipProvider>,
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId('json-editor-dangling-warning'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('warns about layout refs to missing panels', () => {
|
||||
mockUseJsonEditor.mockReturnValue(hookValue({ missingPanelRefs: ['ghost'] }));
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />
|
||||
</TooltipProvider>,
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('json-editor-missing-ref-warning'),
|
||||
).toHaveTextContent('1 layout item references a panel that no longer exists');
|
||||
});
|
||||
|
||||
it('shows the error line and message when invalid', () => {
|
||||
mockUseJsonEditor.mockReturnValue(
|
||||
hookValue({
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { DashboardtypesDashboardSpecDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { findPanelLayoutIssues } from '../danglingPanels';
|
||||
|
||||
const grid = (refs: string[]): unknown => ({
|
||||
kind: 'Grid',
|
||||
spec: { items: refs.map((r) => ({ content: { $ref: r } })) },
|
||||
});
|
||||
|
||||
// Cast a loose fixture to the spec type — the helper is defensive against the
|
||||
// untrusted, hand-edited JSON it runs on.
|
||||
const spec = (value: unknown): DashboardtypesDashboardSpecDTO =>
|
||||
value as DashboardtypesDashboardSpecDTO;
|
||||
|
||||
describe('findPanelLayoutIssues', () => {
|
||||
it('flags nothing when panels and layouts agree', () => {
|
||||
expect(
|
||||
findPanelLayoutIssues(
|
||||
spec({
|
||||
panels: { a: {}, b: {} },
|
||||
layouts: [grid(['#/spec/panels/a', '#/spec/panels/b'])],
|
||||
}),
|
||||
),
|
||||
).toStrictEqual({ danglingPanelIds: [], missingPanelRefs: [] });
|
||||
});
|
||||
|
||||
it('lists panels placed in no layout as dangling', () => {
|
||||
const result = findPanelLayoutIssues(
|
||||
spec({
|
||||
panels: { a: {}, b: {}, c: {} },
|
||||
layouts: [grid(['#/spec/panels/a'])],
|
||||
}),
|
||||
);
|
||||
expect(result.danglingPanelIds.sort()).toStrictEqual(['b', 'c']);
|
||||
expect(result.missingPanelRefs).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('treats a removed/empty layout as orphaning every panel', () => {
|
||||
expect(
|
||||
findPanelLayoutIssues(
|
||||
spec({ panels: { a: {}, b: {} }, layouts: [] }),
|
||||
).danglingPanelIds.sort(),
|
||||
).toStrictEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('lists layout refs to a panel that no longer exists as missing', () => {
|
||||
const result = findPanelLayoutIssues(
|
||||
spec({
|
||||
panels: { a: {} },
|
||||
layouts: [grid(['#/spec/panels/a', '#/spec/panels/ghost'])],
|
||||
}),
|
||||
);
|
||||
expect(result.danglingPanelIds).toStrictEqual([]);
|
||||
expect(result.missingPanelRefs).toStrictEqual(['ghost']);
|
||||
});
|
||||
|
||||
it('handles empty / malformed specs', () => {
|
||||
expect(
|
||||
findPanelLayoutIssues(spec({ panels: {}, layouts: [] })),
|
||||
).toStrictEqual({
|
||||
danglingPanelIds: [],
|
||||
missingPanelRefs: [],
|
||||
});
|
||||
expect(findPanelLayoutIssues(undefined)).toStrictEqual({
|
||||
danglingPanelIds: [],
|
||||
missingPanelRefs: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -203,4 +203,47 @@ describe('useJsonEditor', () => {
|
||||
rerender({ isOpen: true });
|
||||
expect(result.current.draft).toBe(serialized);
|
||||
});
|
||||
|
||||
it('reports panels not placed in any layout as dangling', () => {
|
||||
const withDangling = {
|
||||
...dashboard,
|
||||
spec: { ...dashboard.spec, panels: { p1: {} }, layouts: [] },
|
||||
} as unknown as DashboardtypesGettableDashboardV2DTO;
|
||||
const { result } = renderHook(() =>
|
||||
useJsonEditor({
|
||||
dashboard: withDangling,
|
||||
isOpen: true,
|
||||
onApplied: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.danglingPanelIds).toStrictEqual(['p1']);
|
||||
expect(result.current.missingPanelRefs).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('reports layout refs to panels that no longer exist as missing', () => {
|
||||
const withMissing = {
|
||||
...dashboard,
|
||||
spec: {
|
||||
...dashboard.spec,
|
||||
panels: {},
|
||||
layouts: [
|
||||
{
|
||||
kind: 'Grid',
|
||||
spec: { items: [{ content: { $ref: '#/spec/panels/ghost' } }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as DashboardtypesGettableDashboardV2DTO;
|
||||
const { result } = renderHook(() =>
|
||||
useJsonEditor({
|
||||
dashboard: withMissing,
|
||||
isOpen: true,
|
||||
onApplied: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.missingPanelRefs).toStrictEqual(['ghost']);
|
||||
expect(result.current.danglingPanelIds).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type {
|
||||
DashboardtypesDashboardSpecDTO,
|
||||
DashboardtypesLayoutDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { extractPanelIdFromRef } from '../../utils';
|
||||
|
||||
export interface PanelLayoutIssues {
|
||||
// Panels defined in `spec.panels` that no layout places — they render nowhere.
|
||||
danglingPanelIds: string[];
|
||||
// Panel ids a layout item references that no longer exist in `spec.panels`.
|
||||
missingPanelRefs: string[];
|
||||
}
|
||||
|
||||
const referencedPanelIds = (
|
||||
layouts: DashboardtypesLayoutDTO[],
|
||||
): Set<string> => {
|
||||
const referenced = new Set<string>();
|
||||
layouts.forEach((layout) => {
|
||||
if (layout?.kind !== 'Grid') {
|
||||
return;
|
||||
}
|
||||
(layout.spec?.items ?? []).forEach((item) => {
|
||||
const id = extractPanelIdFromRef(item?.content?.$ref);
|
||||
if (id) {
|
||||
referenced.add(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
return referenced;
|
||||
};
|
||||
|
||||
// The two ways a hand-edited spec can desync panels and layouts: a panel with no
|
||||
// layout slot (renders nowhere), or a layout slot pointing at a panel that was
|
||||
// removed (broken reference). Guarded for untrusted, user-edited JSON.
|
||||
export function findPanelLayoutIssues(
|
||||
spec: DashboardtypesDashboardSpecDTO | undefined,
|
||||
): PanelLayoutIssues {
|
||||
const panels = spec?.panels ?? {};
|
||||
const panelIds = Object.keys(panels);
|
||||
const referenced = referencedPanelIds(spec?.layouts ?? []);
|
||||
|
||||
return {
|
||||
danglingPanelIds: panelIds.filter((id) => !referenced.has(id)),
|
||||
missingPanelRefs: [...referenced].filter((id) => !(id in panels)),
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { updateDashboardV2 } from 'api/generated/services/dashboard';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type {
|
||||
DashboardtypesDashboardSpecDTO,
|
||||
DashboardtypesGettableDashboardV2DTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import { dashboardToUpdatable } from './dashboardToUpdatable';
|
||||
import { findPanelLayoutIssues } from './danglingPanels';
|
||||
import { useDashboardStore } from '../../store/useDashboardStore';
|
||||
|
||||
export interface JsonValidity {
|
||||
@@ -28,6 +32,10 @@ interface Result {
|
||||
validity: JsonValidity;
|
||||
isDirty: boolean;
|
||||
isSaving: boolean;
|
||||
// Panel ids in the draft's `spec.panels` referenced by no layout — orphaned.
|
||||
danglingPanelIds: string[];
|
||||
// Panel ids a layout references that are missing from the draft's `spec.panels`.
|
||||
missingPanelRefs: string[];
|
||||
format: () => void;
|
||||
reset: () => void;
|
||||
apply: () => Promise<void>;
|
||||
@@ -109,6 +117,23 @@ export function useJsonEditor({
|
||||
|
||||
const isDirty = draft !== appliedText;
|
||||
|
||||
const { danglingPanelIds, missingPanelRefs } = useMemo<{
|
||||
danglingPanelIds: string[];
|
||||
missingPanelRefs: string[];
|
||||
}>(() => {
|
||||
if (!validity.valid) {
|
||||
return { danglingPanelIds: [], missingPanelRefs: [] };
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(draft) as {
|
||||
spec?: DashboardtypesDashboardSpecDTO;
|
||||
};
|
||||
return findPanelLayoutIssues(parsed.spec);
|
||||
} catch {
|
||||
return { danglingPanelIds: [], missingPanelRefs: [] };
|
||||
}
|
||||
}, [draft, validity.valid]);
|
||||
|
||||
const format = useCallback((): void => {
|
||||
try {
|
||||
setDraft(JSON.stringify(JSON.parse(draft), null, 2));
|
||||
@@ -138,7 +163,7 @@ export function useJsonEditor({
|
||||
refetch();
|
||||
onApplied();
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
showErrorModal(toAPIError(error as Parameters<typeof toAPIError>[0]));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -159,6 +184,8 @@ export function useJsonEditor({
|
||||
validity,
|
||||
isDirty,
|
||||
isSaving,
|
||||
danglingPanelIds,
|
||||
missingPanelRefs,
|
||||
format,
|
||||
reset,
|
||||
apply,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { DashboardtypesPatchOpDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type {
|
||||
DashboardtypesGettableDashboardV2DTO,
|
||||
DashboardtypesJSONPatchOperationDTO,
|
||||
@@ -54,20 +55,32 @@ function Overview({ dashboard }: OverviewProps): JSX.Element {
|
||||
|
||||
const buildPatch = useCallback((): DashboardtypesJSONPatchOperationDTO[] => {
|
||||
const ops: DashboardtypesJSONPatchOperationDTO[] = [];
|
||||
const op = (
|
||||
operation: DashboardtypesJSONPatchOperationDTO['op'],
|
||||
path: string,
|
||||
value: unknown,
|
||||
): DashboardtypesJSONPatchOperationDTO => ({ op: operation, path, value });
|
||||
const replace = (
|
||||
path: string,
|
||||
value: unknown,
|
||||
): DashboardtypesJSONPatchOperationDTO => ({
|
||||
op: 'replace' as DashboardtypesJSONPatchOperationDTO['op'],
|
||||
path,
|
||||
value,
|
||||
});
|
||||
): DashboardtypesJSONPatchOperationDTO =>
|
||||
op(DashboardtypesPatchOpDTO.replace, path, value);
|
||||
|
||||
if (updatedTitle !== title && updatedTitle !== '') {
|
||||
ops.push(replace('/spec/display/name', updatedTitle));
|
||||
}
|
||||
if (updatedDescription !== description) {
|
||||
ops.push(replace('/spec/display/description', updatedDescription));
|
||||
// `replace` fails when the description doesn't exist yet, so add it when
|
||||
// the current one is empty (`add` creates or replaces the member).
|
||||
ops.push(
|
||||
op(
|
||||
description
|
||||
? DashboardtypesPatchOpDTO.replace
|
||||
: DashboardtypesPatchOpDTO.add,
|
||||
'/spec/display/description',
|
||||
updatedDescription,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (updatedImage !== image) {
|
||||
ops.push(replace('/image', updatedImage));
|
||||
|
||||
@@ -7,6 +7,7 @@ import styles from './DashboardsList.module.scss';
|
||||
interface Props {
|
||||
label: string;
|
||||
count: number;
|
||||
isModified: boolean;
|
||||
canCreate: boolean;
|
||||
onCreate: () => void;
|
||||
}
|
||||
@@ -14,6 +15,7 @@ interface Props {
|
||||
function CommandHeader({
|
||||
label,
|
||||
count,
|
||||
isModified,
|
||||
canCreate,
|
||||
onCreate,
|
||||
}: Props): JSX.Element {
|
||||
@@ -21,6 +23,7 @@ function CommandHeader({
|
||||
<div className={styles.commandHeader}>
|
||||
<div className={styles.headingBlock}>
|
||||
<Typography.Title className={styles.title}>{label}</Typography.Title>
|
||||
{isModified && <span className={styles.dirtyDot} title="Unsaved changes" />}
|
||||
<span className={styles.countPill}>{count}</span>
|
||||
</div>
|
||||
<div className={styles.grow} />
|
||||
|
||||
@@ -93,6 +93,14 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dirtyDot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--warning-background);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.countPill {
|
||||
padding: 2px 9px;
|
||||
border-radius: 999px;
|
||||
|
||||
@@ -10,9 +10,9 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import { combineQueries } from '../../utils/filterQuery';
|
||||
import { useAccumulatedTags } from '../../hooks/useAccumulatedTags';
|
||||
import { useActiveView } from '../../hooks/useActiveView';
|
||||
import { useCreatorOptions } from '../../hooks/useCreatorOptions';
|
||||
import { useDashboardFilters } from '../../hooks/useDashboardFilters';
|
||||
import {
|
||||
usePage,
|
||||
@@ -25,7 +25,6 @@ import { BuiltinViewId } from '../../types';
|
||||
import type { SelectedTag, UpdatedWindow } from '../../types';
|
||||
import type { DashboardListItem } from '../../utils/helpers';
|
||||
import { applyClientView } from '../../utils/views';
|
||||
import type { CreatorOption } from '../FilterZone/FilterChips';
|
||||
import FilterZone from '../FilterZone/FilterZone';
|
||||
import NewDashboardModal from '../NewDashboardModal/NewDashboardModal';
|
||||
import StatusBar from '../StatusBar/StatusBar';
|
||||
@@ -72,13 +71,13 @@ function DashboardsList(): JSX.Element {
|
||||
customViewsLoading,
|
||||
isCustomActive,
|
||||
isModified,
|
||||
viewQuery,
|
||||
clientView,
|
||||
selectView,
|
||||
saveView,
|
||||
saveActiveView,
|
||||
resetView,
|
||||
removeView,
|
||||
renameView,
|
||||
} = useActiveView({
|
||||
filters,
|
||||
applyFilters,
|
||||
@@ -153,13 +152,13 @@ function DashboardsList(): JSX.Element {
|
||||
|
||||
const listParams = useMemo(
|
||||
() => ({
|
||||
query: combineQueries(viewQuery, query) || undefined,
|
||||
query: query || undefined,
|
||||
sort: sortColumn,
|
||||
order: sortOrder,
|
||||
limit: clientView ? CLIENT_VIEW_LIMIT : PAGE_SIZE,
|
||||
offset: clientView ? 0 : (page - 1) * PAGE_SIZE,
|
||||
}),
|
||||
[viewQuery, query, sortColumn, sortOrder, page, clientView],
|
||||
[query, sortColumn, sortOrder, page, clientView],
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -195,24 +194,19 @@ function DashboardsList(): JSX.Element {
|
||||
);
|
||||
const total = clientView ? dashboards.length : (response?.data?.total ?? 0);
|
||||
|
||||
// Creator filter options: distinct authors on the loaded page plus the
|
||||
// current user (so "me" is always selectable). Page-scoped until a members
|
||||
// source backs this.
|
||||
const creatorOptions = useMemo<CreatorOption[]>(() => {
|
||||
const emails = new Set<string>();
|
||||
if (user.email) {
|
||||
emails.add(user.email);
|
||||
}
|
||||
rawDashboards.forEach((d) => {
|
||||
if (d.createdBy) {
|
||||
emails.add(d.createdBy);
|
||||
}
|
||||
});
|
||||
return [...emails].sort().map((email) => ({
|
||||
email,
|
||||
label: email === user.email ? `${email} (me)` : email,
|
||||
}));
|
||||
}, [rawDashboards, user.email]);
|
||||
// Authors present on the loaded page — a fallback for the creator filter until
|
||||
// the org-wide user list resolves.
|
||||
const pageAuthorEmails = useMemo<string[]>(
|
||||
() =>
|
||||
rawDashboards
|
||||
.map((d) => d.createdBy)
|
||||
.filter((email): email is string => !!email),
|
||||
[rawDashboards],
|
||||
);
|
||||
const creatorOptions = useCreatorOptions({
|
||||
currentUserEmail: user.email,
|
||||
fallbackEmails: pageAuthorEmails,
|
||||
});
|
||||
|
||||
// All key:value tags the API reports for the org's dashboards, powering the
|
||||
// Tags filter chip and DSL key suggestions. Accumulated across refetches so
|
||||
@@ -289,8 +283,8 @@ function DashboardsList(): JSX.Element {
|
||||
onSave={saveView}
|
||||
onSaveChanges={saveActiveView}
|
||||
onReset={handleResetView}
|
||||
onClearFilters={handleClearAll}
|
||||
onDelete={handleRemoveView}
|
||||
onRename={renameView}
|
||||
/>
|
||||
<div className={styles.main}>
|
||||
<div className={styles.mainScroll}>
|
||||
@@ -305,6 +299,7 @@ function DashboardsList(): JSX.Element {
|
||||
<CommandHeader
|
||||
label={activeLabel}
|
||||
count={total}
|
||||
isModified={isModified}
|
||||
canCreate={canCreateNewDashboard}
|
||||
onCreate={openCreate}
|
||||
/>
|
||||
|
||||
@@ -6,32 +6,45 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from './ViewsRail.module.scss';
|
||||
|
||||
export const VIEW_NAME_MAX_LENGTH = 128;
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave: (name: string) => void;
|
||||
onSubmit: (name: string) => void;
|
||||
trigger: ReactNode;
|
||||
title: string;
|
||||
confirmLabel: string;
|
||||
initialName?: string;
|
||||
testIdPrefix?: string;
|
||||
}
|
||||
|
||||
function SaveViewPopover({
|
||||
// Name-input popover shared by "save as view" and "rename view"; enforces the
|
||||
// view-name length cap in one place.
|
||||
function ViewNamePopover({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
onSubmit,
|
||||
trigger,
|
||||
title,
|
||||
confirmLabel,
|
||||
initialName = '',
|
||||
testIdPrefix = 'view-name',
|
||||
}: Props): JSX.Element {
|
||||
const [name, setName] = useState('');
|
||||
const [name, setName] = useState(initialName);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setName('');
|
||||
setName(initialName);
|
||||
}
|
||||
}, [open]);
|
||||
}, [open, initialName]);
|
||||
|
||||
const canSave = name.trim().length > 0;
|
||||
const trimmed = name.trim();
|
||||
const canSave = trimmed.length > 0 && trimmed.length <= VIEW_NAME_MAX_LENGTH;
|
||||
|
||||
const handleSave = (): void => {
|
||||
if (canSave) {
|
||||
onSave(name);
|
||||
onSubmit(trimmed);
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
@@ -44,13 +57,14 @@ function SaveViewPopover({
|
||||
trigger={trigger}
|
||||
>
|
||||
<div className={styles.savePopover}>
|
||||
<div className={styles.saveTitle}>Save as view</div>
|
||||
<div className={styles.saveTitle}>{title}</div>
|
||||
<Typography.Text className={styles.saveLabel}>Name</Typography.Text>
|
||||
<Input
|
||||
value={name}
|
||||
autoFocus
|
||||
maxLength={VIEW_NAME_MAX_LENGTH}
|
||||
placeholder="e.g. Prod alerts"
|
||||
testId="save-view-name"
|
||||
testId={`${testIdPrefix}-name`}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||
setName(e.target.value)
|
||||
}
|
||||
@@ -74,10 +88,10 @@ function SaveViewPopover({
|
||||
color="primary"
|
||||
size="sm"
|
||||
disabled={!canSave}
|
||||
testId="save-view-confirm"
|
||||
testId={`${testIdPrefix}-confirm`}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save view
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,4 +99,4 @@ function SaveViewPopover({
|
||||
);
|
||||
}
|
||||
|
||||
export default SaveViewPopover;
|
||||
export default ViewNamePopover;
|
||||
@@ -145,30 +145,40 @@
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.itemAction {
|
||||
// Blended ghost icon overlaid on the row's right edge — absolutely positioned
|
||||
// so it never reserves layout space or affects the row height. Transparent so
|
||||
// the row's hover background shows through; turns red only on its own hover.
|
||||
// Ghost action icons overlaid on the row's right edge — absolutely positioned so
|
||||
// they never reserve layout space or affect row height. Hidden until row hover,
|
||||
// and inert while hidden so they can't intercept clicks meant for the row.
|
||||
.itemActions {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
--button-height: 20px;
|
||||
--button-padding: 0;
|
||||
--button-border-radius: 4px;
|
||||
--button-variant-ghost-color: var(--l3-foreground);
|
||||
--button-variant-ghost-hover-background-color: var(--danger-background);
|
||||
--button-variant-ghost-hover-color: var(--danger-color, #fff);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
// Hidden until row hover, and inert while hidden so it can't intercept clicks
|
||||
// meant for the row.
|
||||
pointer-events: none;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
.row:hover .itemAction {
|
||||
.itemAction {
|
||||
--button-height: 20px;
|
||||
--button-padding: 0;
|
||||
--button-border-radius: 4px;
|
||||
--button-variant-ghost-color: var(--l3-foreground);
|
||||
--button-variant-ghost-hover-background-color: var(--l1-background);
|
||||
--button-variant-ghost-hover-color: var(--l1-foreground);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.itemActionDanger {
|
||||
--button-variant-ghost-hover-background-color: var(--danger-background);
|
||||
--button-variant-ghost-hover-color: var(--danger-color, #fff);
|
||||
}
|
||||
|
||||
.row:hover .itemActions {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,19 @@ import { Modal } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Bookmark, CircleAlert, Plus, Search, Trash2 } from '@signozhq/icons';
|
||||
import {
|
||||
Bookmark,
|
||||
CircleAlert,
|
||||
PenLine,
|
||||
Plus,
|
||||
Search,
|
||||
Trash2,
|
||||
} from '@signozhq/icons';
|
||||
import cx from 'classnames';
|
||||
|
||||
import type { SavedView } from '../../types';
|
||||
import { type BuiltinView } from '../../utils/views';
|
||||
import SaveViewPopover from './SaveViewPopover';
|
||||
import ViewNamePopover from './ViewNamePopover';
|
||||
|
||||
import styles from './ViewsRail.module.scss';
|
||||
|
||||
@@ -24,8 +31,8 @@ interface Props {
|
||||
onSave: (name: string) => void;
|
||||
onSaveChanges: () => void;
|
||||
onReset: () => void;
|
||||
onClearFilters: () => void;
|
||||
onDelete: (id: string) => void;
|
||||
onRename: (id: string, name: string) => void;
|
||||
}
|
||||
|
||||
interface ViewRow {
|
||||
@@ -49,10 +56,11 @@ function ViewsRail({
|
||||
onSave,
|
||||
onSaveChanges,
|
||||
onReset,
|
||||
onClearFilters,
|
||||
onDelete,
|
||||
onRename,
|
||||
}: Props): JSX.Element {
|
||||
const [saveOpen, setSaveOpen] = useState(false);
|
||||
const [renamingId, setRenamingId] = useState<string | null>(null);
|
||||
const [query, setQuery] = useState('');
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
|
||||
@@ -122,20 +130,44 @@ function ViewsRail({
|
||||
)}
|
||||
</Button>
|
||||
{row.deletable && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={styles.itemAction}
|
||||
aria-label="Delete view"
|
||||
title="Delete view"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
confirmDelete(row.id, row.label);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</Button>
|
||||
<div className={styles.itemActions}>
|
||||
<ViewNamePopover
|
||||
open={renamingId === row.id}
|
||||
onOpenChange={(open): void => setRenamingId(open ? row.id : null)}
|
||||
onSubmit={(name): void => onRename(row.id, name)}
|
||||
title="Rename view"
|
||||
confirmLabel="Rename"
|
||||
initialName={row.label}
|
||||
testIdPrefix="rename-view"
|
||||
trigger={
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={styles.itemAction}
|
||||
aria-label="Rename view"
|
||||
title="Rename view"
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
>
|
||||
<PenLine size={12} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={cx(styles.itemAction, styles.itemActionDanger)}
|
||||
aria-label="Delete view"
|
||||
title="Delete view"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
confirmDelete(row.id, row.label);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -145,10 +177,13 @@ function ViewsRail({
|
||||
<aside className={cx(styles.rail, { [styles.collapsed]: collapsed })}>
|
||||
<div className={styles.header}>
|
||||
<h4 className={styles.headerTitle}>Views</h4>
|
||||
<SaveViewPopover
|
||||
<ViewNamePopover
|
||||
open={saveOpen}
|
||||
onOpenChange={setSaveOpen}
|
||||
onSave={onSave}
|
||||
onSubmit={onSave}
|
||||
title="Save as view"
|
||||
confirmLabel="Save view"
|
||||
testIdPrefix="save-view"
|
||||
trigger={
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -268,13 +303,8 @@ function ViewsRail({
|
||||
>
|
||||
Save as new view
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={onClearFilters}
|
||||
>
|
||||
Clear
|
||||
<Button variant="ghost" color="secondary" size="sm" onClick={onReset}>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
|
||||
import {
|
||||
areFilterStatesEqual,
|
||||
combineQueries,
|
||||
DEFAULT_FILTER_STATE,
|
||||
filterStateToQuery,
|
||||
} from '../utils/filterQuery';
|
||||
@@ -15,7 +14,6 @@ import { BuiltinViewId } from '../types';
|
||||
import type { DashboardFilterState, SavedView } from '../types';
|
||||
import {
|
||||
BUILTIN_VIEWS,
|
||||
builtinViewQuery,
|
||||
builtinViewSnapshot,
|
||||
type BuiltinView,
|
||||
isClientView,
|
||||
@@ -42,15 +40,14 @@ export interface UseActiveViewResult {
|
||||
isCustomActive: boolean;
|
||||
// Current filters diverge from the active view's canonical snapshot.
|
||||
isModified: boolean;
|
||||
// Extra server-query fragment the active view contributes, and whether it
|
||||
// constrains the list client-side (pinned/recent).
|
||||
viewQuery: string;
|
||||
// Whether the active view constrains the list client-side (pinned/recent).
|
||||
clientView: boolean;
|
||||
selectView: (id: string) => void;
|
||||
saveView: (name: string) => void;
|
||||
saveActiveView: () => void;
|
||||
resetView: () => void;
|
||||
removeView: (id: string) => void;
|
||||
renameView: (id: string, name: string) => void;
|
||||
}
|
||||
|
||||
// The canonical filter snapshot a saved view "is": the backend stores a flat
|
||||
@@ -127,11 +124,9 @@ export function useActiveView({
|
||||
|
||||
const saveView = useCallback(
|
||||
(name: string): void => {
|
||||
// Fold the current built-in clause + chips into a single query string.
|
||||
const query = combineQueries(
|
||||
builtinViewQuery(activeViewId),
|
||||
filterStateToQuery(filters),
|
||||
);
|
||||
// The active view's clause already lives in the filter state (e.g. Locked
|
||||
// seeds `locked = true` into search), so the chips fold into one query.
|
||||
const query = filterStateToQuery(filters);
|
||||
void (async (): Promise<void> => {
|
||||
const created = await createView({
|
||||
name,
|
||||
@@ -147,15 +142,7 @@ export function useActiveView({
|
||||
}
|
||||
})();
|
||||
},
|
||||
[
|
||||
activeViewId,
|
||||
filters,
|
||||
createView,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
setActiveViewId,
|
||||
applyFilters,
|
||||
],
|
||||
[filters, createView, sortColumn, sortOrder, setActiveViewId, applyFilters],
|
||||
);
|
||||
|
||||
const saveActiveView = useCallback((): void => {
|
||||
@@ -200,6 +187,23 @@ export function useActiveView({
|
||||
[deleteView, activeViewId, setActiveViewId, applyFilters],
|
||||
);
|
||||
|
||||
// Rename only touches the view's name; its stored query/sort/order are preserved.
|
||||
const renameView = useCallback(
|
||||
(id: string, name: string): void => {
|
||||
const view = customViews.find((v) => v.id === id);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
updateView(id, {
|
||||
name,
|
||||
query: view.query,
|
||||
sort: view.sort,
|
||||
order: view.order,
|
||||
});
|
||||
},
|
||||
[customViews, updateView],
|
||||
);
|
||||
|
||||
return {
|
||||
activeViewId,
|
||||
builtinViews: BUILTIN_VIEWS,
|
||||
@@ -207,12 +211,12 @@ export function useActiveView({
|
||||
customViewsLoading,
|
||||
isCustomActive: !!activeCustom,
|
||||
isModified,
|
||||
viewQuery: builtinViewQuery(activeViewId),
|
||||
clientView: isClientView(activeViewId),
|
||||
selectView,
|
||||
saveView,
|
||||
saveActiveView,
|
||||
resetView,
|
||||
removeView,
|
||||
renameView,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useListUsers } from 'api/generated/services/users';
|
||||
|
||||
import type { CreatorOption } from '../components/FilterZone/FilterChips';
|
||||
|
||||
interface Args {
|
||||
currentUserEmail: string;
|
||||
// Authors on the loaded page — kept selectable until the org list resolves.
|
||||
fallbackEmails: string[];
|
||||
}
|
||||
|
||||
// Creator-filter options sourced from the org's full user list, so authors who
|
||||
// aren't on the current page are still selectable (v2 "List users" API).
|
||||
export function useCreatorOptions({
|
||||
currentUserEmail,
|
||||
fallbackEmails,
|
||||
}: Args): CreatorOption[] {
|
||||
const { data } = useListUsers();
|
||||
|
||||
return useMemo<CreatorOption[]>(() => {
|
||||
const users = data?.data ?? [];
|
||||
const emails = new Set<string>();
|
||||
if (currentUserEmail) {
|
||||
emails.add(currentUserEmail);
|
||||
}
|
||||
users.forEach((u) => u.email && emails.add(u.email));
|
||||
// Until the org list resolves (or if it comes back empty), keep the page's
|
||||
// authors selectable so the filter never regresses to just "me".
|
||||
if (users.length === 0) {
|
||||
fallbackEmails.forEach((e) => emails.add(e));
|
||||
}
|
||||
|
||||
const labelFor = (email: string): string => {
|
||||
if (email === currentUserEmail) {
|
||||
return `${email} (me)`;
|
||||
}
|
||||
const match = users.find((u) => u.email === email);
|
||||
return match?.displayName ? `${match.displayName} (${email})` : email;
|
||||
};
|
||||
|
||||
return [...emails].sort().map((email) => ({ email, label: labelFor(email) }));
|
||||
}, [data, currentUserEmail, fallbackEmails]);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import {
|
||||
getListDashboardViewsQueryKey,
|
||||
invalidateListDashboardViews,
|
||||
useCreateDashboardView,
|
||||
useDeleteDashboardView,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
type DashboardtypesDashboardViewDTO,
|
||||
DashboardtypesListOrderDTO,
|
||||
DashboardtypesListSortDTO,
|
||||
type ListDashboardViews200,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import type { SavedView, SavedViewInput } from '../types';
|
||||
@@ -70,9 +72,27 @@ export function useSavedViews(): UseSavedViewsResult {
|
||||
},
|
||||
},
|
||||
});
|
||||
// Rename/save-changes returns the updated view, so patch it into the cached
|
||||
// list inline instead of refetching the whole list.
|
||||
const updateMutation = useUpdateDashboardView({
|
||||
mutation: {
|
||||
onSuccess: invalidate,
|
||||
onSuccess: (response): void => {
|
||||
const updated = response?.data;
|
||||
const key = getListDashboardViewsQueryKey();
|
||||
const prev = queryClient.getQueryData<ListDashboardViews200>(key);
|
||||
if (!updated || !prev) {
|
||||
return;
|
||||
}
|
||||
queryClient.setQueryData<ListDashboardViews200>(key, {
|
||||
...prev,
|
||||
data: {
|
||||
...prev.data,
|
||||
views: (prev.data.views ?? []).map((v) =>
|
||||
v.id === updated.id ? updated : v,
|
||||
),
|
||||
},
|
||||
});
|
||||
},
|
||||
onError: (): void => {
|
||||
toast.error('Failed to update view.');
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Built-in view catalogue + the pure logic that maps a view to how it
|
||||
// constrains the list. Views fall into three mechanisms:
|
||||
// - snapshot: selecting applies a filter snapshot (All, My dashboards, custom)
|
||||
// - query: contributes an extra server clause AND-ed with the chips (Locked)
|
||||
// constrains the list. Views fall into two mechanisms:
|
||||
// - snapshot: selecting applies a filter snapshot — some seed the search box
|
||||
// with DSL (My dashboards → created_by; Locked → locked = true)
|
||||
// - client: constrains by a client-side id set (Favorites, Recently viewed)
|
||||
import { Clock, Layers, Lock, Pin, User } from '@signozhq/icons';
|
||||
|
||||
@@ -49,9 +49,9 @@ export const BUILTIN_VIEWS: BuiltinView[] = [
|
||||
export const isClientView = (id: string): boolean =>
|
||||
id === BuiltinViewId.Pinned || id === BuiltinViewId.Recent;
|
||||
|
||||
// Extra server query fragment a built-in view contributes (AND-ed with chips).
|
||||
export const builtinViewQuery = (id: string): string =>
|
||||
id === BuiltinViewId.Locked ? 'locked = true' : '';
|
||||
// DSL the Locked view seeds into the search box, so the constraint is visible
|
||||
// (and editable) rather than applied invisibly behind the scenes.
|
||||
export const LOCKED_QUERY = 'locked = true';
|
||||
|
||||
// The canonical filter snapshot a built-in view applies when selected. `null`
|
||||
// for ids that aren't built-in (custom views carry their own snapshot).
|
||||
@@ -65,10 +65,11 @@ export const builtinViewSnapshot = (
|
||||
...DEFAULT_FILTER_STATE,
|
||||
createdBy: userEmail ? [userEmail] : [],
|
||||
};
|
||||
case BuiltinViewId.Locked:
|
||||
return { ...DEFAULT_FILTER_STATE, search: LOCKED_QUERY };
|
||||
case BuiltinViewId.All:
|
||||
case BuiltinViewId.Pinned:
|
||||
case BuiltinViewId.Recent:
|
||||
case BuiltinViewId.Locked:
|
||||
return { ...DEFAULT_FILTER_STATE };
|
||||
default:
|
||||
return null;
|
||||
|
||||
@@ -33,15 +33,19 @@
|
||||
}
|
||||
|
||||
&.filter-visible {
|
||||
// Width is owned by ResizableBox (inline style); this section is the
|
||||
// ResizableBox root, so it stays position: relative for the drag handle.
|
||||
.log-quick-filter-left-section {
|
||||
width: 260px;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.resizable-box__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.quick-filters-container {
|
||||
flex: 1;
|
||||
@@ -50,7 +54,9 @@
|
||||
}
|
||||
|
||||
.log-module-right-section {
|
||||
width: calc(100% - 260px);
|
||||
flex: 1;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import { defaultTo, isEmpty, isNull } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ResizableBox } from 'periscope/components/ResizableBox';
|
||||
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
|
||||
import { EventSourceProvider } from 'providers/EventSource';
|
||||
import { Warning } from 'types/api';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -44,9 +46,23 @@ import { ExplorerViews } from './utils';
|
||||
|
||||
import './LogsExplorer.styles.scss';
|
||||
|
||||
const QUICK_FILTERS_DEFAULT_WIDTH = 260;
|
||||
const QUICK_FILTERS_MIN_WIDTH = 240;
|
||||
const QUICK_FILTERS_MAX_WIDTH = 500;
|
||||
|
||||
function LogsExplorer(): JSX.Element {
|
||||
const [showLiveLogs, setShowLiveLogs] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
initialWidth: quickFiltersInitialWidth,
|
||||
persistWidth: persistQuickFiltersWidth,
|
||||
} = usePanelWidth({
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_LOGS,
|
||||
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
|
||||
minWidth: QUICK_FILTERS_MIN_WIDTH,
|
||||
maxWidth: QUICK_FILTERS_MAX_WIDTH,
|
||||
});
|
||||
|
||||
// Get panel type from URL
|
||||
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
|
||||
@@ -226,14 +242,25 @@ function LogsExplorer(): JSX.Element {
|
||||
className={cx('logs-module-page', showFilters ? 'filter-visible' : '')}
|
||||
>
|
||||
{showFilters && (
|
||||
<section className={cx('log-quick-filter-left-section')}>
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
|
||||
initialWidth={quickFiltersInitialWidth}
|
||||
minWidth={QUICK_FILTERS_MIN_WIDTH}
|
||||
maxWidth={QUICK_FILTERS_MAX_WIDTH}
|
||||
onResize={persistQuickFiltersWidth}
|
||||
resetToDefaultOnDoubleClick
|
||||
withHandle
|
||||
className="log-quick-filter-left-section"
|
||||
handleTestId="quick-filters-resize-handle"
|
||||
>
|
||||
<QuickFilters
|
||||
className="qf-logs-explorer"
|
||||
signal={SignalType.LOGS}
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
/>
|
||||
</section>
|
||||
</ResizableBox>
|
||||
)}
|
||||
<section className={cx('log-module-right-section')}>
|
||||
<Toolbar
|
||||
|
||||
@@ -18,6 +18,17 @@
|
||||
z-index: 10;
|
||||
background: var(--l2-border);
|
||||
|
||||
// Extend the interactive area beyond the 1px visual line so the handle
|
||||
// is easy to grab and double-click, without changing its appearance.
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: var(--primary);
|
||||
@@ -55,4 +66,29 @@
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Visible grip indicator (opt-in via the `withHandle` prop). Purely visual —
|
||||
// pointer events fall through to the handle so it still owns drag + reset.
|
||||
&__grip {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 26px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 4px;
|
||||
background: var(--l1-background);
|
||||
color: var(--l2-foreground);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__handle:hover &__grip,
|
||||
&__handle:active &__grip {
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GripVertical } from '@signozhq/icons';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import './ResizableBox.styles.scss';
|
||||
@@ -11,15 +12,28 @@ export interface ResizableBoxProps {
|
||||
// resize (width). Dragging the handle away from the content grows the box;
|
||||
// dragging it toward the content shrinks it.
|
||||
handle?: ResizableBoxHandle;
|
||||
// Canonical default size, and the target that double-click reset restores to.
|
||||
defaultHeight?: number;
|
||||
minHeight?: number;
|
||||
maxHeight?: number;
|
||||
defaultWidth?: number;
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
// Starting size when different from the default (e.g. a persisted value).
|
||||
// Falls back to defaultWidth/defaultHeight when omitted, preserving the
|
||||
// behavior of callers that don't opt in.
|
||||
initialWidth?: number;
|
||||
initialHeight?: number;
|
||||
// When true, double-clicking the handle resets the size to
|
||||
// defaultWidth/defaultHeight and fires onResize with that value.
|
||||
resetToDefaultOnDoubleClick?: boolean;
|
||||
// When true, renders a visible grip indicator on the handle so it is
|
||||
// discoverable as a draggable affordance.
|
||||
withHandle?: boolean;
|
||||
onResize?: (size: number) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
handleTestId?: string;
|
||||
}
|
||||
|
||||
function ResizableBox({
|
||||
@@ -31,13 +45,22 @@ function ResizableBox({
|
||||
defaultWidth = 200,
|
||||
minWidth = 50,
|
||||
maxWidth = Infinity,
|
||||
initialWidth,
|
||||
initialHeight,
|
||||
resetToDefaultOnDoubleClick = false,
|
||||
withHandle = false,
|
||||
onResize,
|
||||
disabled = false,
|
||||
className,
|
||||
handleTestId,
|
||||
}: ResizableBoxProps): JSX.Element {
|
||||
const isHorizontal = handle === 'left' || handle === 'right';
|
||||
const isStartHandle = handle === 'top' || handle === 'left';
|
||||
const [size, setSize] = useState(isHorizontal ? defaultWidth : defaultHeight);
|
||||
const [size, setSize] = useState(
|
||||
isHorizontal
|
||||
? (initialWidth ?? defaultWidth)
|
||||
: (initialHeight ?? defaultHeight),
|
||||
);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
@@ -83,6 +106,21 @@ function ResizableBox({
|
||||
],
|
||||
);
|
||||
|
||||
const handleDoubleClick = useCallback((): void => {
|
||||
if (!resetToDefaultOnDoubleClick) {
|
||||
return;
|
||||
}
|
||||
const nextSize = isHorizontal ? defaultWidth : defaultHeight;
|
||||
setSize(nextSize);
|
||||
onResize?.(nextSize);
|
||||
}, [
|
||||
resetToDefaultOnDoubleClick,
|
||||
isHorizontal,
|
||||
defaultWidth,
|
||||
defaultHeight,
|
||||
onResize,
|
||||
]);
|
||||
|
||||
const containerStyle = disabled
|
||||
? undefined
|
||||
: isHorizontal
|
||||
@@ -99,7 +137,22 @@ function ResizableBox({
|
||||
style={containerStyle}
|
||||
>
|
||||
<div className="resizable-box__content">{children}</div>
|
||||
{!disabled && <div className={handleClass} onMouseDown={handleMouseDown} />}
|
||||
{!disabled && (
|
||||
<div
|
||||
role="separator"
|
||||
aria-orientation={isHorizontal ? 'vertical' : 'horizontal'}
|
||||
className={handleClass}
|
||||
onMouseDown={handleMouseDown}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
data-testid={handleTestId}
|
||||
>
|
||||
{withHandle && (
|
||||
<span className="resizable-box__grip">
|
||||
<GripVertical size={12} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import ResizableBox from '../ResizableBox';
|
||||
|
||||
const HANDLE_TEST_ID = 'resize-handle';
|
||||
|
||||
describe('ResizableBox', () => {
|
||||
it('starts at defaultWidth when initialWidth is omitted', () => {
|
||||
render(
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={260}
|
||||
handleTestId={HANDLE_TEST_ID}
|
||||
>
|
||||
<div>content</div>
|
||||
</ResizableBox>,
|
||||
);
|
||||
|
||||
const box = screen.getByTestId(HANDLE_TEST_ID).parentElement as HTMLElement;
|
||||
expect(box.style.width).toBe('260px');
|
||||
});
|
||||
|
||||
it('starts at initialWidth when provided', () => {
|
||||
render(
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={260}
|
||||
initialWidth={340}
|
||||
handleTestId={HANDLE_TEST_ID}
|
||||
>
|
||||
<div>content</div>
|
||||
</ResizableBox>,
|
||||
);
|
||||
|
||||
const box = screen.getByTestId(HANDLE_TEST_ID).parentElement as HTMLElement;
|
||||
expect(box.style.width).toBe('340px');
|
||||
});
|
||||
|
||||
it('resets to defaultWidth and fires onResize on double-click when enabled', () => {
|
||||
const onResize = jest.fn();
|
||||
render(
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={260}
|
||||
initialWidth={480}
|
||||
onResize={onResize}
|
||||
resetToDefaultOnDoubleClick
|
||||
handleTestId={HANDLE_TEST_ID}
|
||||
>
|
||||
<div>content</div>
|
||||
</ResizableBox>,
|
||||
);
|
||||
|
||||
const handle = screen.getByTestId(HANDLE_TEST_ID);
|
||||
const box = handle.parentElement as HTMLElement;
|
||||
expect(box.style.width).toBe('480px');
|
||||
|
||||
fireEvent.doubleClick(handle);
|
||||
|
||||
expect(box.style.width).toBe('260px');
|
||||
expect(onResize).toHaveBeenCalledWith(260);
|
||||
});
|
||||
|
||||
it('does nothing on double-click when reset is not enabled', () => {
|
||||
const onResize = jest.fn();
|
||||
render(
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={260}
|
||||
initialWidth={480}
|
||||
onResize={onResize}
|
||||
handleTestId={HANDLE_TEST_ID}
|
||||
>
|
||||
<div>content</div>
|
||||
</ResizableBox>,
|
||||
);
|
||||
|
||||
const handle = screen.getByTestId(HANDLE_TEST_ID);
|
||||
const box = handle.parentElement as HTMLElement;
|
||||
|
||||
fireEvent.doubleClick(handle);
|
||||
|
||||
expect(box.style.width).toBe('480px');
|
||||
expect(onResize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders a visible grip only when withHandle is set', () => {
|
||||
const { rerender, container } = render(
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={260}
|
||||
handleTestId={HANDLE_TEST_ID}
|
||||
>
|
||||
<div>content</div>
|
||||
</ResizableBox>,
|
||||
);
|
||||
expect(container.querySelector('.resizable-box__grip')).toBeNull();
|
||||
|
||||
rerender(
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={260}
|
||||
withHandle
|
||||
handleTestId={HANDLE_TEST_ID}
|
||||
>
|
||||
<div>content</div>
|
||||
</ResizableBox>,
|
||||
);
|
||||
expect(container.querySelector('.resizable-box__grip')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('clamps drag to maxWidth and reports the clamped size via onResize', () => {
|
||||
const onResize = jest.fn();
|
||||
render(
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={260}
|
||||
minWidth={240}
|
||||
maxWidth={500}
|
||||
onResize={onResize}
|
||||
handleTestId={HANDLE_TEST_ID}
|
||||
>
|
||||
<div>content</div>
|
||||
</ResizableBox>,
|
||||
);
|
||||
|
||||
const handle = screen.getByTestId(HANDLE_TEST_ID);
|
||||
const box = handle.parentElement as HTMLElement;
|
||||
|
||||
fireEvent.mouseDown(handle, { clientX: 0 });
|
||||
fireEvent.mouseMove(document, { clientX: 1000 });
|
||||
fireEvent.mouseUp(document);
|
||||
|
||||
expect(box.style.width).toBe('500px');
|
||||
expect(onResize).toHaveBeenLastCalledWith(500);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
import usePanelWidth from '../usePanelWidth';
|
||||
|
||||
jest.mock('api/browser/localstorage/get');
|
||||
jest.mock('api/browser/localstorage/set');
|
||||
|
||||
const mockedGet = getLocalStorageKey as jest.MockedFunction<
|
||||
typeof getLocalStorageKey
|
||||
>;
|
||||
const mockedSet = setLocalStorageKey as jest.MockedFunction<
|
||||
typeof setLocalStorageKey
|
||||
>;
|
||||
|
||||
const ARGS = {
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_LOGS,
|
||||
defaultWidth: 260,
|
||||
minWidth: 240,
|
||||
maxWidth: 500,
|
||||
};
|
||||
|
||||
describe('usePanelWidth', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('returns defaultWidth when nothing is persisted', () => {
|
||||
mockedGet.mockReturnValue(null);
|
||||
const { result } = renderHook(() => usePanelWidth(ARGS));
|
||||
expect(result.current.initialWidth).toBe(260);
|
||||
});
|
||||
|
||||
it('returns the persisted width when present', () => {
|
||||
mockedGet.mockReturnValue('340');
|
||||
const { result } = renderHook(() => usePanelWidth(ARGS));
|
||||
expect(result.current.initialWidth).toBe(340);
|
||||
});
|
||||
|
||||
it('clamps an out-of-bounds persisted width on read', () => {
|
||||
mockedGet.mockReturnValue('9999');
|
||||
const { result } = renderHook(() => usePanelWidth(ARGS));
|
||||
expect(result.current.initialWidth).toBe(500);
|
||||
});
|
||||
|
||||
it('falls back to defaultWidth for an invalid persisted value', () => {
|
||||
mockedGet.mockReturnValue('not-a-number');
|
||||
const { result } = renderHook(() => usePanelWidth(ARGS));
|
||||
expect(result.current.initialWidth).toBe(260);
|
||||
});
|
||||
|
||||
it('persists a clamped width (debounced)', () => {
|
||||
mockedGet.mockReturnValue(null);
|
||||
const { result } = renderHook(() => usePanelWidth(ARGS));
|
||||
|
||||
act(() => {
|
||||
result.current.persistWidth(320);
|
||||
jest.advanceTimersByTime(200);
|
||||
});
|
||||
|
||||
expect(mockedSet).toHaveBeenCalledWith(
|
||||
LOCALSTORAGE.QUICK_FILTERS_WIDTH_LOGS,
|
||||
'320',
|
||||
);
|
||||
});
|
||||
|
||||
it('clamps below-min widths before persisting', () => {
|
||||
mockedGet.mockReturnValue(null);
|
||||
const { result } = renderHook(() => usePanelWidth(ARGS));
|
||||
|
||||
act(() => {
|
||||
result.current.persistWidth(10);
|
||||
jest.advanceTimersByTime(200);
|
||||
});
|
||||
|
||||
expect(mockedSet).toHaveBeenCalledWith(
|
||||
LOCALSTORAGE.QUICK_FILTERS_WIDTH_LOGS,
|
||||
'240',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
|
||||
const PERSIST_DEBOUNCE_MS = 150;
|
||||
|
||||
interface UsePanelWidthArgs {
|
||||
/** Per-page localStorage key the width is persisted under. */
|
||||
storageKey: LOCALSTORAGE;
|
||||
/** Canonical default width, used when nothing is persisted. */
|
||||
defaultWidth: number;
|
||||
minWidth: number;
|
||||
maxWidth: number;
|
||||
}
|
||||
|
||||
interface UsePanelWidthReturn {
|
||||
/** Width to start from: the persisted value (clamped) or the default. */
|
||||
initialWidth: number;
|
||||
/** Clamp and persist a width. Debounced to avoid a write per mousemove. */
|
||||
persistWidth: (width: number) => void;
|
||||
}
|
||||
|
||||
const clamp = (value: number, min: number, max: number): number =>
|
||||
Math.min(max, Math.max(min, value));
|
||||
|
||||
/**
|
||||
* Per-page localStorage persistence for a resizable panel width. Mirrors the
|
||||
* getLocalStorageKey/setLocalStorageKey idiom used for the trace span-details
|
||||
* panel position. Pairs with ResizableBox: feed initialWidth into its
|
||||
* initialWidth prop and persistWidth into its onResize.
|
||||
*/
|
||||
function usePanelWidth({
|
||||
storageKey,
|
||||
defaultWidth,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
}: UsePanelWidthArgs): UsePanelWidthReturn {
|
||||
// Read once on mount. Kept in a ref so a re-render doesn't re-read storage.
|
||||
const initialWidthRef = useRef<number | null>(null);
|
||||
if (initialWidthRef.current === null) {
|
||||
const stored = getLocalStorageKey(storageKey);
|
||||
const parsed = stored !== null && stored !== '' ? Number(stored) : NaN;
|
||||
initialWidthRef.current = Number.isFinite(parsed)
|
||||
? clamp(parsed, minWidth, maxWidth)
|
||||
: defaultWidth;
|
||||
}
|
||||
|
||||
const debouncedWrite = useMemo(
|
||||
() =>
|
||||
debounce((width: number): void => {
|
||||
setLocalStorageKey(storageKey, String(width));
|
||||
}, PERSIST_DEBOUNCE_MS),
|
||||
[storageKey],
|
||||
);
|
||||
|
||||
const persistWidth = useCallback(
|
||||
(width: number): void => {
|
||||
debouncedWrite(clamp(width, minWidth, maxWidth));
|
||||
},
|
||||
[debouncedWrite, minWidth, maxWidth],
|
||||
);
|
||||
|
||||
return { initialWidth: initialWidthRef.current, persistWidth };
|
||||
}
|
||||
|
||||
export default usePanelWidth;
|
||||
66
frontend/src/utils/__tests__/linkifyText.test.tsx
Normal file
66
frontend/src/utils/__tests__/linkifyText.test.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
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('');
|
||||
});
|
||||
});
|
||||
71
frontend/src/utils/linkifyText.tsx
Normal file
71
frontend/src/utils/linkifyText.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
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;
|
||||
}
|
||||
@@ -117,8 +117,16 @@ var podsSpec = checkSpec{
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{"k8s.pod.phase"},
|
||||
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",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
@@ -149,6 +157,10 @@ 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,
|
||||
},
|
||||
{
|
||||
@@ -182,6 +194,10 @@ var deploymentsSpec = checkSpec{
|
||||
"k8s.deployment.desired",
|
||||
"k8s.deployment.available",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.status_reason",
|
||||
"k8s.container.status.reason",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
@@ -220,6 +236,10 @@ 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,
|
||||
},
|
||||
{
|
||||
@@ -258,6 +278,10 @@ var statefulsetsSpec = checkSpec{
|
||||
"k8s.statefulset.desired_pods",
|
||||
"k8s.statefulset.current_pods",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.status_reason",
|
||||
"k8s.container.status.reason",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
@@ -298,6 +322,10 @@ var jobsSpec = checkSpec{
|
||||
"k8s.job.failed_pods",
|
||||
"k8s.job.successful_pods",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.status_reason",
|
||||
"k8s.container.status.reason",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
@@ -324,8 +352,12 @@ var namespacesSpec = checkSpec{
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{"k8s.pod.phase"},
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{"k8s.pod.phase"},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.status_reason",
|
||||
"k8s.container.status.reason",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
@@ -360,6 +392,10 @@ 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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -20,6 +20,8 @@ func buildClusterRecords(
|
||||
metadataMap map[string]map[string]string,
|
||||
nodeConditionCountsMap map[string]nodeConditionCounts,
|
||||
podPhaseCountsMap map[string]podPhaseCounts,
|
||||
podStatusCounts map[string]podStatusCounts,
|
||||
resourceCounts map[string]map[string]int64,
|
||||
) []inframonitoringtypes.ClusterRecord {
|
||||
metricsMap := parseFullQueryResponse(resp, groupBy)
|
||||
|
||||
@@ -69,6 +71,19 @@ func buildClusterRecords(
|
||||
}
|
||||
}
|
||||
|
||||
if podStatusCountsForGroup, ok := podStatusCounts[compositeKey]; ok {
|
||||
record.PodCountsByStatus = podStatusCountsToResponse(podStatusCountsForGroup)
|
||||
}
|
||||
|
||||
if counts, ok := resourceCounts[compositeKey]; ok {
|
||||
record.Counts.Nodes = counts[inframonitoringtypes.NodeNameAttrKey]
|
||||
record.Counts.Namespaces = counts[inframonitoringtypes.NamespaceNameAttrKey]
|
||||
record.Counts.Deployments = counts[inframonitoringtypes.DeploymentNameAttrKey]
|
||||
record.Counts.DaemonSets = counts[inframonitoringtypes.DaemonSetNameAttrKey]
|
||||
record.Counts.Jobs = counts[inframonitoringtypes.JobNameAttrKey]
|
||||
record.Counts.StatefulSets = counts[inframonitoringtypes.StatefulSetNameAttrKey]
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
@@ -27,12 +29,34 @@ 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{
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
// clusterCountAttrKeys are the resource attributes whose distinct values are
|
||||
// counted per cluster. Node name is read from the node metric universe, while
|
||||
// namespace + workload names come from the pod metric universe — both unioned
|
||||
// into clusterCountMetricNamesList.
|
||||
var clusterCountAttrKeys = []string{
|
||||
inframonitoringtypes.NodeNameAttrKey,
|
||||
inframonitoringtypes.NamespaceNameAttrKey,
|
||||
inframonitoringtypes.DeploymentNameAttrKey,
|
||||
inframonitoringtypes.DaemonSetNameAttrKey,
|
||||
inframonitoringtypes.JobNameAttrKey,
|
||||
inframonitoringtypes.StatefulSetNameAttrKey,
|
||||
}
|
||||
|
||||
// clusterMetricNamesListForCounts is the metric universe for per-cluster distinct
|
||||
// counts. It unions the pod universe (carries namespace + workload owner labels)
|
||||
// with the cluster/node universe (carries k8s.node.name), so a single query can
|
||||
// count nodes, namespaces, and workloads per cluster. Overlapping pod
|
||||
// phase/status metrics are left in — harmless in a metric_name IN (...) list.
|
||||
var clusterMetricNamesListForCounts = slices.Concat(podsTableMetricNamesList, clustersTableMetricNamesList)
|
||||
|
||||
var orderByToClustersQueryNames = map[string][]string{
|
||||
inframonitoringtypes.ClustersOrderByCPU: {"A"},
|
||||
inframonitoringtypes.ClustersOrderByCPUAllocatable: {"B"},
|
||||
|
||||
@@ -19,6 +19,7 @@ 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)
|
||||
|
||||
@@ -77,6 +78,10 @@ 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
|
||||
|
||||
@@ -23,6 +23,8 @@ 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",
|
||||
|
||||
@@ -19,6 +19,7 @@ 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)
|
||||
|
||||
@@ -77,6 +78,10 @@ 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
|
||||
|
||||
@@ -23,6 +23,8 @@ 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",
|
||||
|
||||
@@ -440,6 +440,30 @@ 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.
|
||||
//
|
||||
@@ -738,3 +762,170 @@ func (m *module) getMetadata(
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getPerGroupDistinctCounts returns, per groupBy combination, the exact distinct
|
||||
// count of each attr in attrNames within the time range and metric universe.
|
||||
// It mirrors getMetadata: fingerprints come from the samples table, labels are
|
||||
// read from the timeseries table (raw only, or raw+reduced union when reduction
|
||||
// is enabled), and the user filter is merged with the page-groups IN clauses.
|
||||
// The returned map keys group column values by "\x00", mapping to attr -> count.
|
||||
func (m *module) getPerGroupDistinctCounts(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
start, end int64,
|
||||
filter *qbtypes.Filter,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
pageGroups []map[string]string,
|
||||
attrNames []string,
|
||||
metricNames []string,
|
||||
) (map[string]map[string]int64, error) {
|
||||
if len(pageGroups) == 0 || len(groupBy) == 0 {
|
||||
return map[string]map[string]int64{}, nil
|
||||
}
|
||||
if len(attrNames) == 0 {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "attrNames must not be empty")
|
||||
}
|
||||
if len(metricNames) == 0 {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricNames must not be empty")
|
||||
}
|
||||
|
||||
// Merge user filter with page-groups IN clauses.
|
||||
userFilterExpr := ""
|
||||
if filter != nil {
|
||||
userFilterExpr = filter.Expression
|
||||
}
|
||||
pageGroupsFilterExpr := buildPageGroupsFilterExpr(pageGroups)
|
||||
mergedFilterExpr := mergeFilterExpressions(userFilterExpr, pageGroupsFilterExpr)
|
||||
|
||||
reductionEnabled := m.fl.BooleanOrEmpty(ctx, flagger.FeatureEnableMetricsReduction, featuretypes.NewFlaggerEvaluationContext(orgID))
|
||||
|
||||
// Step-floor the window and pick the right tables — same bounds the QB v5
|
||||
// metric querier uses (see alignedMetricWindow / getMetadata).
|
||||
samplesStartMs, flooredEndMs, tsAdjustedStartMs, distributedTimeSeriesTbl, _, _, localSamplesTbl := alignedMetricWindow(start, end)
|
||||
|
||||
fpSB := m.buildSamplesTblFingerprintSubQuery(metricNames, localSamplesTbl, samplesStartMs, flooredEndMs)
|
||||
|
||||
groupByCols := make([]string, len(groupBy))
|
||||
for i, key := range groupBy {
|
||||
groupByCols[i] = key.Name
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
// SELECT: one JSONExtractString per groupBy col + one uniqExactIf per attr.
|
||||
selectCols := make([]string, 0, len(groupByCols)+len(attrNames))
|
||||
for _, col := range groupByCols {
|
||||
selectCols = append(selectCols,
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS %s", sb.Var(col), quoteIdentifier(col)),
|
||||
)
|
||||
}
|
||||
for _, attr := range attrNames {
|
||||
// Guard on != '' so a series missing the attr isn't counted as one empty value.
|
||||
extract := fmt.Sprintf("JSONExtractString(labels, %s)", sb.Var(attr))
|
||||
selectCols = append(selectCols,
|
||||
fmt.Sprintf("uniqExactIf(%s, %s != '') AS %s", extract, extract, quoteIdentifier(attr)),
|
||||
)
|
||||
}
|
||||
sb.Select(selectCols...)
|
||||
|
||||
if reductionEnabled {
|
||||
var filterClause *sqlbuilder.WhereClause
|
||||
if mergedFilterExpr != "" {
|
||||
var err error
|
||||
filterClause, err = m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
reducedFpSB := m.buildReducedSamplesTblFingerprintSubQuery(metricNames, samplesStartMs, flooredEndMs)
|
||||
|
||||
rawSrc := sqlbuilder.NewSelectBuilder()
|
||||
rawSrc.Select("labels")
|
||||
rawSrc.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, distributedTimeSeriesTbl))
|
||||
rawSrc.Where(
|
||||
rawSrc.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
rawSrc.GE("unix_milli", tsAdjustedStartMs),
|
||||
rawSrc.LE("unix_milli", flooredEndMs),
|
||||
fmt.Sprintf("fingerprint IN (%s)", rawSrc.Var(fpSB)),
|
||||
)
|
||||
if filterClause != nil {
|
||||
rawSrc.AddWhereClause(sqlbuilder.CopyWhereClause(filterClause))
|
||||
}
|
||||
|
||||
reducedSrc := sqlbuilder.NewSelectBuilder()
|
||||
reducedSrc.Select("labels")
|
||||
reducedSrc.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.TimeseriesV4ReducedTableName))
|
||||
reducedSrc.Where(
|
||||
reducedSrc.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
reducedSrc.GE("unix_milli", tsAdjustedStartMs),
|
||||
reducedSrc.LE("unix_milli", flooredEndMs),
|
||||
fmt.Sprintf("fingerprint IN (%s)", reducedSrc.Var(reducedFpSB)),
|
||||
)
|
||||
if filterClause != nil {
|
||||
reducedSrc.AddWhereClause(sqlbuilder.CopyWhereClause(filterClause))
|
||||
}
|
||||
|
||||
sb.From(sb.BuilderAs(sqlbuilder.UnionAll(rawSrc, reducedSrc), "series"))
|
||||
} else {
|
||||
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, distributedTimeSeriesTbl))
|
||||
sb.Where(
|
||||
sb.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
sb.GE("unix_milli", tsAdjustedStartMs),
|
||||
sb.LE("unix_milli", flooredEndMs),
|
||||
fmt.Sprintf("fingerprint IN (%s)", sb.Var(fpSB)),
|
||||
)
|
||||
if mergedFilterExpr != "" {
|
||||
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filterClause != nil {
|
||||
sb.AddWhereClause(filterClause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groupByAliases := make([]string, 0, len(groupByCols))
|
||||
for _, col := range groupByCols {
|
||||
groupByAliases = append(groupByAliases, quoteIdentifier(col))
|
||||
}
|
||||
sb.GroupBy(groupByAliases...)
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
result := make(map[string]map[string]int64)
|
||||
for rows.Next() {
|
||||
groupVals := make([]string, len(groupByCols))
|
||||
counts := make([]uint64, len(attrNames))
|
||||
|
||||
scanPtrs := make([]any, 0, len(groupByCols)+len(attrNames))
|
||||
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
|
||||
}
|
||||
|
||||
attrCounts := make(map[string]int64, len(attrNames))
|
||||
for i, attr := range attrNames {
|
||||
attrCounts[attr] = int64(counts[i])
|
||||
}
|
||||
result[compositeKeyFromList(groupVals)] = attrCounts
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -26,6 +26,60 @@ 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
|
||||
|
||||
@@ -19,6 +19,7 @@ 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)
|
||||
|
||||
@@ -85,6 +86,10 @@ 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
|
||||
|
||||
@@ -23,6 +23,8 @@ 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",
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type module struct {
|
||||
@@ -213,23 +214,37 @@ func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, hostsFilterExpr, req.GroupBy, pageGroups, m.newListHostsQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Compute per-group active/inactive host counts.
|
||||
// When host.name is in groupBy, each row = one host, so counts are derived
|
||||
// directly from activeHostsMap in buildHostRecords (no extra query needed).
|
||||
// When host.name is not in groupBy, we need to run an additional query to get the counts per group for the current page,
|
||||
// using the same filter expression as the main query (including user filters + page groups IN clause).
|
||||
hostCounts := make(map[string]groupHostStatusCounts)
|
||||
isHostNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, inframonitoringtypes.HostNameAttrKey)
|
||||
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
hostCounts = make(map[string]groupHostStatusCounts)
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
|
||||
if !isHostNameInGroupBy {
|
||||
hostCounts, err = m.getPerGroupHostStatusCounts(ctx, orgID, req, hostsTableMetricNamesList, pageGroups, sinceUnixMilli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
hostCounts, err = m.getPerGroupHostStatusCounts(gCtx, orgID, req, hostsTableMetricNamesList, pageGroups, sinceUnixMilli)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildHostRecords(isHostNameInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, activeHostsMap, hostCounts)
|
||||
@@ -299,19 +314,45 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newPodsTableListQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
phaseCounts map[string]podPhaseCounts
|
||||
statusCounts map[string]podStatusCounts
|
||||
statusWarning *qbtypes.QueryWarnData
|
||||
restartCounts map[string]int64
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
restartCounts, err = m.getPerGroupPodRestartCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isPodUIDInGroupBy := isKeyInGroupByAttrs(req.GroupBy, podUIDAttrKey)
|
||||
resp.Records = buildPodRecords(isPodUIDInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, req.End)
|
||||
resp.Warning = queryResp.Warning
|
||||
resp.Records = buildPodRecords(isPodUIDInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, statusCounts, restartCounts, req.End)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, statusWarning)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -377,24 +418,45 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newNodesTableListQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodeConditionCounts, err := m.getPerGroupNodeConditionCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
nodeConditionCounts map[string]nodeConditionCounts
|
||||
podPhaseCounts map[string]podPhaseCounts
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
podPhaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podPhaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isNodeNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, inframonitoringtypes.NodeNameAttrKey)
|
||||
resp.Records = buildNodeRecords(isNodeNameInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCounts, podPhaseCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
resp.Records = buildNodeRecords(isNodeNameInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCounts, podPhaseCounts, podStatusCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -460,18 +522,44 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newNamespacesTableListQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
phaseCounts map[string]podPhaseCounts
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
resourceCounts map[string]map[string]int64
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups, namespaceCountAttrKeys, namespacesTableMetricNamesList)
|
||||
return err
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(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)
|
||||
resp.Warning = queryResp.Warning
|
||||
resp.Records = buildNamespaceRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts, resourceCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -537,25 +625,52 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newClustersTableListQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// With default groupBy [k8s.cluster.name], counts are bucketed per cluster;
|
||||
// with a custom groupBy, they aggregate across clusters in that group.
|
||||
nodeConditionCountsMap, err := m.getPerGroupNodeConditionCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
nodeConditionCountsMap map[string]nodeConditionCounts
|
||||
podPhaseCountsMap map[string]podPhaseCounts
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
resourceCounts map[string]map[string]int64
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
nodeConditionCountsMap, err = m.getPerGroupNodeConditionCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podPhaseCountsMap, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups, clusterCountAttrKeys, clusterMetricNamesListForCounts)
|
||||
return err
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
podPhaseCountsMap, err := m.getPerGroupPodPhaseCounts(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)
|
||||
resp.Warning = queryResp.Warning
|
||||
resp.Records = buildClusterRecords(queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCountsMap, podPhaseCountsMap, podStatusCounts, resourceCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -705,18 +820,38 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newDeploymentsTableListQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
phaseCounts map[string]podPhaseCounts
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(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)
|
||||
resp.Warning = queryResp.Warning
|
||||
resp.Records = buildDeploymentRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -788,20 +923,40 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newStatefulSetsTableListQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Pods owned by a StatefulSet carry k8s.statefulset.name as a resource attribute,
|
||||
// so default-groupBy gives per-statefulset phase counts automatically.
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
phaseCounts map[string]podPhaseCounts
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildStatefulSetRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
resp.Records = buildStatefulSetRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -873,20 +1028,40 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newJobsTableListQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Pods owned by a Job carry k8s.job.name as a resource attribute, so default-groupBy
|
||||
// gives per-job phase counts automatically.
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
phaseCounts map[string]podPhaseCounts
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildJobRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
resp.Records = buildJobRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -958,20 +1133,40 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newDaemonSetsTableListQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Pods owned by a DaemonSet carry k8s.daemonset.name as a resource attribute,
|
||||
// so default-groupBy gives per-daemonset phase counts automatically.
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
var (
|
||||
queryResp *qbtypes.QueryRangeResponse
|
||||
phaseCounts map[string]podPhaseCounts
|
||||
podStatusCounts map[string]podStatusCounts
|
||||
podStatusWarning *qbtypes.QueryWarnData
|
||||
)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
return err
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildDaemonSetRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
resp.Records = buildDaemonSetRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ func buildNamespaceRecords(
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
metadataMap map[string]map[string]string,
|
||||
phaseCounts map[string]podPhaseCounts,
|
||||
podStatusCounts map[string]podStatusCounts,
|
||||
resourceCounts map[string]map[string]int64,
|
||||
) []inframonitoringtypes.NamespaceRecord {
|
||||
metricsMap := parseFullQueryResponse(resp, groupBy)
|
||||
|
||||
@@ -52,6 +54,17 @@ func buildNamespaceRecords(
|
||||
}
|
||||
}
|
||||
|
||||
if podStatusCountsForGroup, ok := podStatusCounts[compositeKey]; ok {
|
||||
record.PodCountsByStatus = podStatusCountsToResponse(podStatusCountsForGroup)
|
||||
}
|
||||
|
||||
if counts, ok := resourceCounts[compositeKey]; ok {
|
||||
record.Counts.Deployments = counts[inframonitoringtypes.DeploymentNameAttrKey]
|
||||
record.Counts.DaemonSets = counts[inframonitoringtypes.DaemonSetNameAttrKey]
|
||||
record.Counts.Jobs = counts[inframonitoringtypes.JobNameAttrKey]
|
||||
record.Counts.StatefulSets = counts[inframonitoringtypes.StatefulSetNameAttrKey]
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
|
||||
@@ -23,6 +23,8 @@ 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{
|
||||
@@ -30,6 +32,16 @@ var namespaceAttrKeysForMetadata = []string{
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
// namespaceCountAttrKeys are the workload resource attributes whose distinct
|
||||
// values are counted per namespace. They are read from the pod metric universe,
|
||||
// which carries the owner workload names for each pod series.
|
||||
var namespaceCountAttrKeys = []string{
|
||||
inframonitoringtypes.DeploymentNameAttrKey,
|
||||
inframonitoringtypes.DaemonSetNameAttrKey,
|
||||
inframonitoringtypes.JobNameAttrKey,
|
||||
inframonitoringtypes.StatefulSetNameAttrKey,
|
||||
}
|
||||
|
||||
var orderByToNamespacesQueryNames = map[string][]string{
|
||||
inframonitoringtypes.NamespacesOrderByCPU: {"A"},
|
||||
inframonitoringtypes.NamespacesOrderByMemory: {"D"},
|
||||
|
||||
@@ -26,6 +26,7 @@ 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)
|
||||
|
||||
@@ -86,6 +87,10 @@ 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
|
||||
|
||||
@@ -26,6 +26,8 @@ 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{
|
||||
|
||||
@@ -26,6 +26,8 @@ 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)
|
||||
@@ -38,6 +40,8 @@ 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,
|
||||
@@ -95,6 +99,57 @@ 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.
|
||||
@@ -334,3 +389,519 @@ 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
|
||||
}
|
||||
|
||||
@@ -8,11 +8,26 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
podUIDAttrKey = "k8s.pod.uid"
|
||||
podStartTimeAttrKey = "k8s.pod.start_time"
|
||||
podPhaseMetricName = "k8s.pod.phase"
|
||||
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"
|
||||
)
|
||||
|
||||
// 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,
|
||||
@@ -29,6 +44,9 @@ 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{
|
||||
|
||||
@@ -19,6 +19,7 @@ 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)
|
||||
|
||||
@@ -77,6 +78,10 @@ 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
|
||||
|
||||
@@ -23,6 +23,8 @@ 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",
|
||||
|
||||
@@ -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}
|
||||
return &module{store: store, flagger: flagger, querier: querier}
|
||||
}
|
||||
|
||||
func (module *module) List(ctx context.Context, orgID valuer.UUID, offset, limit int, search string, isOverride *bool) ([]*llmpricingruletypes.LLMPricingRule, int, error) {
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
const DefaultMaxConcurrentQueries = 8
|
||||
|
||||
type SkipResourceFingerprint struct {
|
||||
Enabled bool `yaml:"enabled" mapstructure:"enabled"`
|
||||
// If count of fingerprint is above threshold, skip the fingerprint subquery and filter on main table instead.
|
||||
@@ -37,7 +39,7 @@ func newConfig() factory.Config {
|
||||
// Default values
|
||||
CacheTTL: 168 * time.Hour,
|
||||
FluxInterval: 5 * time.Minute,
|
||||
MaxConcurrentQueries: 4,
|
||||
MaxConcurrentQueries: DefaultMaxConcurrentQueries,
|
||||
SkipResourceFingerprint: SkipResourceFingerprint{
|
||||
Enabled: false,
|
||||
Threshold: 100000,
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
@@ -35,20 +36,21 @@ var (
|
||||
)
|
||||
|
||||
type querier struct {
|
||||
logger *slog.Logger
|
||||
fl flagger.Flagger
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
metadataStore telemetrytypes.MetadataStore
|
||||
promEngine prometheus.Prometheus
|
||||
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
|
||||
logStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation]
|
||||
auditStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation]
|
||||
metricStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
|
||||
meterStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
|
||||
traceOperatorStmtBuilder qbtypes.TraceOperatorStatementBuilder
|
||||
bucketCache BucketCache
|
||||
liveDataRefresh time.Duration
|
||||
builderConfig builderConfig
|
||||
logger *slog.Logger
|
||||
fl flagger.Flagger
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
metadataStore telemetrytypes.MetadataStore
|
||||
promEngine prometheus.Prometheus
|
||||
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
|
||||
logStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation]
|
||||
auditStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation]
|
||||
metricStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
|
||||
meterStmtBuilder qbtypes.StatementBuilder[qbtypes.MetricAggregation]
|
||||
traceOperatorStmtBuilder qbtypes.TraceOperatorStatementBuilder
|
||||
bucketCache BucketCache
|
||||
liveDataRefresh time.Duration
|
||||
builderConfig builderConfig
|
||||
maxConcurrentQueries int
|
||||
}
|
||||
|
||||
var _ Querier = (*querier)(nil)
|
||||
@@ -67,25 +69,30 @@ func New(
|
||||
bucketCache BucketCache,
|
||||
flagger flagger.Flagger,
|
||||
logTraceIDWindowPadding time.Duration,
|
||||
maxConcurrentQueries int,
|
||||
) *querier {
|
||||
querierSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/querier")
|
||||
if maxConcurrentQueries <= 0 {
|
||||
maxConcurrentQueries = DefaultMaxConcurrentQueries
|
||||
}
|
||||
return &querier{
|
||||
logger: querierSettings.Logger(),
|
||||
fl: flagger,
|
||||
telemetryStore: telemetryStore,
|
||||
metadataStore: metadataStore,
|
||||
promEngine: promEngine,
|
||||
traceStmtBuilder: traceStmtBuilder,
|
||||
logStmtBuilder: logStmtBuilder,
|
||||
auditStmtBuilder: auditStmtBuilder,
|
||||
metricStmtBuilder: metricStmtBuilder,
|
||||
meterStmtBuilder: meterStmtBuilder,
|
||||
traceOperatorStmtBuilder: traceOperatorStmtBuilder,
|
||||
bucketCache: bucketCache,
|
||||
liveDataRefresh: 5 * time.Second,
|
||||
logger: querierSettings.Logger(),
|
||||
fl: flagger,
|
||||
telemetryStore: telemetryStore,
|
||||
metadataStore: metadataStore,
|
||||
promEngine: promEngine,
|
||||
traceStmtBuilder: traceStmtBuilder,
|
||||
logStmtBuilder: logStmtBuilder,
|
||||
auditStmtBuilder: auditStmtBuilder,
|
||||
metricStmtBuilder: metricStmtBuilder,
|
||||
meterStmtBuilder: meterStmtBuilder,
|
||||
traceOperatorStmtBuilder: traceOperatorStmtBuilder,
|
||||
bucketCache: bucketCache,
|
||||
liveDataRefresh: 5 * time.Second,
|
||||
builderConfig: builderConfig{
|
||||
logTraceIDWindowPaddingMS: uint64(logTraceIDWindowPadding.Milliseconds()),
|
||||
},
|
||||
maxConcurrentQueries: maxConcurrentQueries,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,30 +614,40 @@ func (q *querier) run(
|
||||
return false
|
||||
}
|
||||
|
||||
for name, query := range qs {
|
||||
// Skip cache if NoCache is set, or if cache is not available
|
||||
if req.NoCache || q.bucketCache == nil || query.Fingerprint() == "" {
|
||||
if req.NoCache {
|
||||
q.logger.DebugContext(ctx, "NoCache flag set, bypassing cache", slog.String("query", name))
|
||||
} else {
|
||||
q.logger.InfoContext(ctx, "no bucket cache or fingerprint, executing query", slog.String("fingerprint", query.Fingerprint()))
|
||||
names := maps.Keys(qs)
|
||||
slices.Sort(names)
|
||||
queryResults := make([]*qbtypes.Result, len(names))
|
||||
|
||||
// sem limits how many queries run at once for this request. The same
|
||||
// limit covers the missing-range queries in executeWithCache. sem is held
|
||||
// only while a query is running, never while waiting for other
|
||||
// goroutines, so the two levels cannot deadlock.
|
||||
sem := make(chan struct{}, q.maxConcurrentQueries)
|
||||
|
||||
eg, egCtx := errgroup.WithContext(ctx)
|
||||
for i, name := range names {
|
||||
query := qs[name]
|
||||
eg.Go(func() error {
|
||||
// Skip cache if NoCache is set, or if cache is not available
|
||||
if req.NoCache || q.bucketCache == nil || query.Fingerprint() == "" {
|
||||
if req.NoCache {
|
||||
q.logger.DebugContext(egCtx, "NoCache flag set, bypassing cache", slog.String("query", name))
|
||||
} else {
|
||||
q.logger.InfoContext(egCtx, "no bucket cache or fingerprint, executing query", slog.String("fingerprint", query.Fingerprint()))
|
||||
}
|
||||
sem <- struct{}{}
|
||||
result, err := query.Execute(egCtx)
|
||||
<-sem
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queryResults[i] = result
|
||||
return nil
|
||||
}
|
||||
result, err := query.Execute(ctx)
|
||||
qbEvent.HasData = qbEvent.HasData || hasData(result)
|
||||
|
||||
result, err := q.executeWithCache(egCtx, orgID, query, steps[name], sem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results[name] = result.Value
|
||||
warnings = append(warnings, result.Warnings...)
|
||||
warningsDocURL = result.WarningsDocURL
|
||||
stats.RowsScanned += result.Stats.RowsScanned
|
||||
stats.BytesScanned += result.Stats.BytesScanned
|
||||
stats.DurationMS += result.Stats.DurationMS
|
||||
} else {
|
||||
result, err := q.executeWithCache(ctx, orgID, query, steps[name], req.NoCache)
|
||||
qbEvent.HasData = qbEvent.HasData || hasData(result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
switch v := result.Value.(type) {
|
||||
case *qbtypes.TimeSeriesData:
|
||||
@@ -640,14 +657,23 @@ func (q *querier) run(
|
||||
case *qbtypes.RawData:
|
||||
v.QueryName = name
|
||||
}
|
||||
queryResults[i] = result
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results[name] = result.Value
|
||||
warnings = append(warnings, result.Warnings...)
|
||||
warningsDocURL = result.WarningsDocURL
|
||||
stats.RowsScanned += result.Stats.RowsScanned
|
||||
stats.BytesScanned += result.Stats.BytesScanned
|
||||
stats.DurationMS += result.Stats.DurationMS
|
||||
}
|
||||
for i, name := range names {
|
||||
result := queryResults[i]
|
||||
qbEvent.HasData = qbEvent.HasData || hasData(result)
|
||||
results[name] = result.Value
|
||||
warnings = append(warnings, result.Warnings...)
|
||||
warningsDocURL = result.WarningsDocURL
|
||||
stats.RowsScanned += result.Stats.RowsScanned
|
||||
stats.BytesScanned += result.Stats.BytesScanned
|
||||
stats.DurationMS += result.Stats.DurationMS
|
||||
}
|
||||
|
||||
gomaps.Copy(results, preseededResults)
|
||||
@@ -707,8 +733,9 @@ func (q *querier) run(
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// executeWithCache executes a query using the bucket cache.
|
||||
func (q *querier) executeWithCache(ctx context.Context, orgID valuer.UUID, query qbtypes.Query, step qbtypes.Step, _ bool) (*qbtypes.Result, error) {
|
||||
// executeWithCache executes a query using the bucket cache. sem limits how
|
||||
// many queries run at once for the whole request.
|
||||
func (q *querier) executeWithCache(ctx context.Context, orgID valuer.UUID, query qbtypes.Query, step qbtypes.Step, sem chan struct{}) (*qbtypes.Result, error) {
|
||||
// Get cached data and missing ranges
|
||||
cachedResult, missingRanges := q.bucketCache.GetMissRanges(ctx, orgID, query, step)
|
||||
|
||||
@@ -721,7 +748,9 @@ func (q *querier) executeWithCache(ctx context.Context, orgID valuer.UUID, query
|
||||
if cachedResult == nil && len(missingRanges) == 1 {
|
||||
startMs, endMs := query.Window()
|
||||
if missingRanges[0].From == startMs && missingRanges[0].To == endMs {
|
||||
sem <- struct{}{}
|
||||
result, err := query.Execute(ctx)
|
||||
<-sem
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -740,7 +769,6 @@ func (q *querier) executeWithCache(ctx context.Context, orgID valuer.UUID, query
|
||||
slog.Int("missing_ranges_count", len(missingRanges)),
|
||||
slog.Any("ranges", missingRanges))
|
||||
|
||||
sem := make(chan struct{}, 4)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i, timeRange := range missingRanges {
|
||||
@@ -777,7 +805,9 @@ func (q *querier) executeWithCache(ctx context.Context, orgID valuer.UUID, query
|
||||
if err != nil {
|
||||
// If any query failed, fall back to full execution
|
||||
q.logger.ErrorContext(ctx, "parallel query execution failed", errors.Attr(err))
|
||||
sem <- struct{}{}
|
||||
result, err := query.Execute(ctx)
|
||||
<-sem
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ package querier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cmock "github.com/SigNoz/clickhouse-go-mock"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
@@ -54,7 +56,8 @@ func TestQueryRange_MetricTypeMissing(t *testing.T) {
|
||||
nil, // traceOperatorStmtBuilder
|
||||
nil, // bucketCache
|
||||
flaggertest.New(t), // flagger
|
||||
0,
|
||||
0, // logTraceIDWindowPadding
|
||||
0, // maxConcurrentQueries
|
||||
)
|
||||
|
||||
req := &qbtypes.QueryRangeRequest{
|
||||
@@ -125,7 +128,8 @@ func TestQueryRange_MetricTypeFromStore(t *testing.T) {
|
||||
nil, // traceOperatorStmtBuilder
|
||||
nil, // bucketCache
|
||||
flaggertest.New(t), // flagger
|
||||
0,
|
||||
0, // logTraceIDWindowPadding
|
||||
0, // maxConcurrentQueries
|
||||
)
|
||||
|
||||
req := &qbtypes.QueryRangeRequest{
|
||||
@@ -155,3 +159,149 @@ func TestQueryRange_MetricTypeFromStore(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
}
|
||||
|
||||
type fakeQuery struct {
|
||||
execute func(ctx context.Context) (*qbtypes.Result, error)
|
||||
}
|
||||
|
||||
func (f *fakeQuery) Fingerprint() string { return "" }
|
||||
func (f *fakeQuery) Window() (uint64, uint64) { return 0, 0 }
|
||||
func (f *fakeQuery) Execute(ctx context.Context) (*qbtypes.Result, error) { return f.execute(ctx) }
|
||||
|
||||
func chQueryEnvelopes(names []string) []qbtypes.QueryEnvelope {
|
||||
envelopes := make([]qbtypes.QueryEnvelope, 0, len(names))
|
||||
for _, name := range names {
|
||||
envelopes = append(envelopes, qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeClickHouseSQL,
|
||||
Spec: qbtypes.ClickHouseQuery{Name: name},
|
||||
})
|
||||
}
|
||||
return envelopes
|
||||
}
|
||||
|
||||
func TestRunExecutesQueriesConcurrently(t *testing.T) {
|
||||
names := []string{"A", "B", "C", "D", "E"}
|
||||
numQueries := len(names)
|
||||
|
||||
q := &querier{
|
||||
logger: instrumentationtest.New().Logger(),
|
||||
maxConcurrentQueries: numQueries,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var started atomic.Int32
|
||||
allStarted := make(chan struct{})
|
||||
|
||||
qs := make(map[string]qbtypes.Query, numQueries)
|
||||
for _, name := range names {
|
||||
qs[name] = &fakeQuery{execute: func(ctx context.Context) (*qbtypes.Result, error) {
|
||||
if int(started.Add(1)) == numQueries {
|
||||
close(allStarted)
|
||||
}
|
||||
select {
|
||||
case <-allStarted:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
return &qbtypes.Result{
|
||||
Type: qbtypes.RequestTypeScalar,
|
||||
Value: &qbtypes.ScalarData{QueryName: name},
|
||||
Stats: qbtypes.ExecStats{RowsScanned: 1, BytesScanned: 2, DurationMS: 3},
|
||||
}, nil
|
||||
}}
|
||||
}
|
||||
|
||||
req := &qbtypes.QueryRangeRequest{
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{Queries: chQueryEnvelopes(names)},
|
||||
}
|
||||
resp, err := q.run(ctx, valuer.GenerateUUID(), qs, req, nil, &qbtypes.QBEvent{}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.Len(t, resp.Data.Results, numQueries)
|
||||
assert.Equal(t, uint64(numQueries), resp.Meta.RowsScanned)
|
||||
assert.Equal(t, uint64(2*numQueries), resp.Meta.BytesScanned)
|
||||
assert.Equal(t, uint64(3*numQueries), resp.Meta.DurationMS)
|
||||
}
|
||||
|
||||
func TestRunRespectsMaxConcurrentQueries(t *testing.T) {
|
||||
const limit = 2
|
||||
names := []string{"A", "B", "C", "D", "E", "F", "G", "H"}
|
||||
|
||||
q := &querier{
|
||||
logger: instrumentationtest.New().Logger(),
|
||||
maxConcurrentQueries: limit,
|
||||
}
|
||||
|
||||
var running, maxRunning atomic.Int32
|
||||
qs := make(map[string]qbtypes.Query, len(names))
|
||||
for _, name := range names {
|
||||
qs[name] = &fakeQuery{execute: func(ctx context.Context) (*qbtypes.Result, error) {
|
||||
cur := running.Add(1)
|
||||
defer running.Add(-1)
|
||||
for {
|
||||
m := maxRunning.Load()
|
||||
if cur <= m || maxRunning.CompareAndSwap(m, cur) {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
return &qbtypes.Result{
|
||||
Type: qbtypes.RequestTypeScalar,
|
||||
Value: &qbtypes.ScalarData{QueryName: name},
|
||||
}, nil
|
||||
}}
|
||||
}
|
||||
|
||||
req := &qbtypes.QueryRangeRequest{
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{Queries: chQueryEnvelopes(names)},
|
||||
}
|
||||
resp, err := q.run(context.Background(), valuer.GenerateUUID(), qs, req, nil, &qbtypes.QBEvent{}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.Len(t, resp.Data.Results, len(names))
|
||||
assert.LessOrEqual(t, maxRunning.Load(), int32(limit), "running queries must not exceed maxConcurrentQueries")
|
||||
}
|
||||
|
||||
func TestRunQueryErrorCancelsSiblings(t *testing.T) {
|
||||
q := &querier{
|
||||
logger: instrumentationtest.New().Logger(),
|
||||
maxConcurrentQueries: 4,
|
||||
}
|
||||
|
||||
bStarted := make(chan struct{})
|
||||
var bCanceled atomic.Bool
|
||||
|
||||
qs := map[string]qbtypes.Query{
|
||||
// fails once B is running.
|
||||
"A": &fakeQuery{execute: func(ctx context.Context) (*qbtypes.Result, error) {
|
||||
select {
|
||||
case <-bStarted:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
return nil, errors.NewInternalf(errors.CodeInternal, "query A failed")
|
||||
}},
|
||||
// blocks until its context is canceled by A's failure.
|
||||
"B": &fakeQuery{execute: func(ctx context.Context) (*qbtypes.Result, error) {
|
||||
close(bStarted)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
bCanceled.Store(true)
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(10 * time.Second):
|
||||
return nil, errors.NewInternalf(errors.CodeInternal, "query B was never canceled")
|
||||
}
|
||||
}},
|
||||
}
|
||||
|
||||
req := &qbtypes.QueryRangeRequest{
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{Queries: chQueryEnvelopes([]string{"A", "B"})},
|
||||
}
|
||||
_, err := q.run(context.Background(), valuer.GenerateUUID(), qs, req, nil, &qbtypes.QBEvent{}, nil)
|
||||
require.ErrorContains(t, err, "query A failed")
|
||||
assert.True(t, bCanceled.Load(), "query B should be canceled once query A fails")
|
||||
}
|
||||
|
||||
@@ -193,5 +193,6 @@ func newProvider(
|
||||
bucketCache,
|
||||
flagger,
|
||||
cfg.LogTraceIDWindowPadding,
|
||||
cfg.MaxConcurrentQueries,
|
||||
), nil
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ func prepareQuerierForMetrics(t *testing.T, telemetryStore telemetrystore.Teleme
|
||||
nil, // bucketCache
|
||||
flagger,
|
||||
0,
|
||||
0, // maxConcurrentQueries (0 means default)
|
||||
), metadataStore
|
||||
}
|
||||
|
||||
@@ -110,6 +111,7 @@ func prepareQuerierForLogs(t *testing.T, telemetryStore telemetrystore.Telemetry
|
||||
nil, // bucketCache
|
||||
fl,
|
||||
5*time.Minute, // logTraceIDWindowPadding
|
||||
0, // maxConcurrentQueries (0 means default)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -158,5 +160,6 @@ func prepareQuerierForTraces(t *testing.T, telemetryStore telemetrystore.Telemet
|
||||
nil, // bucketCache
|
||||
fl,
|
||||
0,
|
||||
0, // maxConcurrentQueries (0 means default)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,16 @@ type ClusterRecord struct {
|
||||
ClusterMemoryAllocatable float64 `json:"clusterMemoryAllocatable" required:"true"`
|
||||
NodeCountsByReadiness NodeCountsByReadiness `json:"nodeCountsByReadiness" required:"true"`
|
||||
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
|
||||
Counts struct {
|
||||
Nodes int64 `json:"nodes" required:"true"`
|
||||
Namespaces int64 `json:"namespaces" required:"true"`
|
||||
Deployments int64 `json:"deployments" required:"true"`
|
||||
DaemonSets int64 `json:"daemonSets" required:"true"`
|
||||
Jobs int64 `json:"jobs" required:"true"`
|
||||
StatefulSets int64 `json:"statefulSets" required:"true"`
|
||||
} `json:"counts" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableClusters is the request body for the v2 clusters list API.
|
||||
|
||||
@@ -17,16 +17,17 @@ 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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ 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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ 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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,18 @@ 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"`
|
||||
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"`
|
||||
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
|
||||
Counts struct {
|
||||
Deployments int64 `json:"deployments" required:"true"`
|
||||
DaemonSets int64 `json:"daemonSets" required:"true"`
|
||||
Jobs int64 `json:"jobs" required:"true"`
|
||||
StatefulSets int64 `json:"statefulSets" required:"true"`
|
||||
} `json:"counts" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableNamespaces is the request body for the v2 namespaces list API.
|
||||
|
||||
@@ -28,6 +28,7 @@ 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"`
|
||||
|
||||
@@ -26,18 +26,49 @@ 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"`
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// PostablePods is the request body for the v2 pods list API.
|
||||
|
||||
@@ -26,6 +26,70 @@ 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 (
|
||||
|
||||
@@ -27,6 +27,7 @@ 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"`
|
||||
}
|
||||
|
||||
|
||||
52
tests/fixtures/inframonitoring.py
vendored
Normal file
52
tests/fixtures/inframonitoring.py
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
"""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
|
||||
@@ -34,3 +34,30 @@
|
||||
{"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}
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c1-n1","k8s.node.uid":"acc-c1-n1-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:00:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c1-n1","k8s.node.uid":"acc-c1-n1-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:02:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c1-n1","k8s.node.uid":"acc-c1-n1-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n1-p-uid","k8s.pod.name":"acc-c1-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n1","k8s.cluster.name":"acc-cluster-1"},"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-c1-n1-p-uid","k8s.pod.name":"acc-c1-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n1","k8s.cluster.name":"acc-cluster-1"},"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-c1-n1-p-uid","k8s.pod.name":"acc-c1-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n1","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n1-p-uid","k8s.pod.name":"acc-c1-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n1","k8s.cluster.name":"acc-cluster-1","k8s.deployment.name":"dep-a"},"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-c1-n1-p-uid","k8s.pod.name":"acc-c1-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n1","k8s.cluster.name":"acc-cluster-1","k8s.deployment.name":"dep-a"},"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-c1-n1-p-uid","k8s.pod.name":"acc-c1-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n1","k8s.cluster.name":"acc-cluster-1","k8s.deployment.name":"dep-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c1-n2","k8s.node.uid":"acc-c1-n2-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c1-n2","k8s.node.uid":"acc-c1-n2-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c1-n2","k8s.node.uid":"acc-c1-n2-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
@@ -31,9 +31,9 @@
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c1-n2","k8s.node.uid":"acc-c1-n2-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:00:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c1-n2","k8s.node.uid":"acc-c1-n2-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:02:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c1-n2","k8s.node.uid":"acc-c1-n2-uid","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n2-p-uid","k8s.pod.name":"acc-c1-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1"},"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-c1-n2-p-uid","k8s.pod.name":"acc-c1-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1"},"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-c1-n2-p-uid","k8s.pod.name":"acc-c1-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c1-n2-p-uid","k8s.pod.name":"acc-c1-n2-p","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1","k8s.statefulset.name":"sts-a"},"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-c1-n2-p-uid","k8s.pod.name":"acc-c1-n2-p","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1","k8s.statefulset.name":"sts-a"},"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-c1-n2-p-uid","k8s.pod.name":"acc-c1-n2-p","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c1-n2","k8s.cluster.name":"acc-cluster-1","k8s.statefulset.name":"sts-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n1","k8s.node.uid":"acc-c2-n1-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n1","k8s.node.uid":"acc-c2-n1-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n1","k8s.node.uid":"acc-c2-n1-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
@@ -49,9 +49,9 @@
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n1","k8s.node.uid":"acc-c2-n1-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n1","k8s.node.uid":"acc-c2-n1-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n1","k8s.node.uid":"acc-c2-n1-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n1-p-uid","k8s.pod.name":"acc-c2-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n1","k8s.cluster.name":"acc-cluster-2"},"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-c2-n1-p-uid","k8s.pod.name":"acc-c2-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n1","k8s.cluster.name":"acc-cluster-2"},"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-c2-n1-p-uid","k8s.pod.name":"acc-c2-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n1","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n1-p-uid","k8s.pod.name":"acc-c2-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n1","k8s.cluster.name":"acc-cluster-2","k8s.deployment.name":"dep-b"},"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-c2-n1-p-uid","k8s.pod.name":"acc-c2-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n1","k8s.cluster.name":"acc-cluster-2","k8s.deployment.name":"dep-b"},"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-c2-n1-p-uid","k8s.pod.name":"acc-c2-n1-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n1","k8s.cluster.name":"acc-cluster-2","k8s.deployment.name":"dep-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n2","k8s.node.uid":"acc-c2-n2-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n2","k8s.node.uid":"acc-c2-n2-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n2","k8s.node.uid":"acc-c2-n2-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
@@ -67,9 +67,9 @@
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n2","k8s.node.uid":"acc-c2-n2-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n2","k8s.node.uid":"acc-c2-n2-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n2","k8s.node.uid":"acc-c2-n2-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n2-p-uid","k8s.pod.name":"acc-c2-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n2","k8s.cluster.name":"acc-cluster-2"},"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-c2-n2-p-uid","k8s.pod.name":"acc-c2-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n2","k8s.cluster.name":"acc-cluster-2"},"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-c2-n2-p-uid","k8s.pod.name":"acc-c2-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n2","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n2-p-uid","k8s.pod.name":"acc-c2-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n2","k8s.cluster.name":"acc-cluster-2","k8s.daemonset.name":"ds-b"},"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-c2-n2-p-uid","k8s.pod.name":"acc-c2-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n2","k8s.cluster.name":"acc-cluster-2","k8s.daemonset.name":"ds-b"},"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-c2-n2-p-uid","k8s.pod.name":"acc-c2-n2-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n2","k8s.cluster.name":"acc-cluster-2","k8s.daemonset.name":"ds-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n3","k8s.node.uid":"acc-c2-n3-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n3","k8s.node.uid":"acc-c2-n3-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.cpu.usage","labels":{"k8s.node.name":"acc-c2-n3","k8s.node.uid":"acc-c2-n3-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":1.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
@@ -85,6 +85,6 @@
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n3","k8s.node.uid":"acc-c2-n3-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n3","k8s.node.uid":"acc-c2-n3-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.node.condition_ready","labels":{"k8s.node.name":"acc-c2-n3","k8s.node.uid":"acc-c2-n3-uid","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":1,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n3-p-uid","k8s.pod.name":"acc-c2-n3-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n3","k8s.cluster.name":"acc-cluster-2"},"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-c2-n3-p-uid","k8s.pod.name":"acc-c2-n3-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n3","k8s.cluster.name":"acc-cluster-2"},"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-c2-n3-p-uid","k8s.pod.name":"acc-c2-n3-p","k8s.namespace.name":"ns-x","k8s.node.name":"acc-c2-n3","k8s.cluster.name":"acc-cluster-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-c2-n3-p-uid","k8s.pod.name":"acc-c2-n3-p","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c2-n3","k8s.cluster.name":"acc-cluster-2","k8s.job.name":"job-b"},"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-c2-n3-p-uid","k8s.pod.name":"acc-c2-n3-p","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c2-n3","k8s.cluster.name":"acc-cluster-2","k8s.job.name":"job-b"},"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-c2-n3-p-uid","k8s.pod.name":"acc-c2-n3-p","k8s.namespace.name":"ns-y","k8s.node.name":"acc-c2-n3","k8s.cluster.name":"acc-cluster-2","k8s.job.name":"job-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
"succeeded": 0,
|
||||
"failed": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"counts": {
|
||||
"nodes": 2,
|
||||
"namespaces": 2,
|
||||
"deployments": 1,
|
||||
"daemonSets": 0,
|
||||
"jobs": 0,
|
||||
"statefulSets": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -34,6 +42,14 @@
|
||||
"succeeded": 0,
|
||||
"failed": 0,
|
||||
"unknown": 0
|
||||
},
|
||||
"counts": {
|
||||
"nodes": 3,
|
||||
"namespaces": 2,
|
||||
"deployments": 1,
|
||||
"daemonSets": 1,
|
||||
"jobs": 1,
|
||||
"statefulSets": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -151,3 +151,30 @@
|
||||
{"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}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"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-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"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-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"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-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"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-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"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-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"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-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"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-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"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-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:00:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:02:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"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-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"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-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"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-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"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-p1a-uid","k8s.pod.name":"acc-p1a","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-1"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"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-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"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-p1b-uid","k8s.pod.name":"acc-p1b","k8s.namespace.name":"acc-ns-1","k8s.cluster.name":"cluster-x","k8s.deployment.name":"web-2"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:00:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:02:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:04:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"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-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"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-p2a-uid","k8s.pod.name":"acc-p2a","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.deployment.name":"api"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:00:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:02:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:04:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"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-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"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-p2b-uid","k8s.pod.name":"acc-p2b","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.daemonset.name":"agent"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.75,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:00:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:02:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:04:00+00:00","value":200000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"acc-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"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-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"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-p2c-uid","k8s.pod.name":"acc-p2c","k8s.namespace.name":"acc-ns-2","k8s.cluster.name":"cluster-y","k8s.job.name":"batch"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
"namespaceName": "acc-ns-1",
|
||||
"namespaceCPU": 1.0,
|
||||
"namespaceMemory": 200000000.0,
|
||||
"podCountsByPhase": {"pending": 0, "running": 2, "succeeded": 0, "failed": 0, "unknown": 0}
|
||||
"podCountsByPhase": {"pending": 0, "running": 2, "succeeded": 0, "failed": 0, "unknown": 0},
|
||||
"counts": {"deployments": 2, "daemonSets": 0, "jobs": 0, "statefulSets": 0}
|
||||
},
|
||||
{
|
||||
"namespaceName": "acc-ns-2",
|
||||
"namespaceCPU": 2.25,
|
||||
"namespaceMemory": 600000000.0,
|
||||
"podCountsByPhase": {"pending": 0, "running": 3, "succeeded": 0, "failed": 0, "unknown": 0}
|
||||
"podCountsByPhase": {"pending": 0, "running": 3, "succeeded": 0, "failed": 0, "unknown": 0},
|
||||
"counts": {"deployments": 1, "daemonSets": 1, "jobs": 1, "statefulSets": 0}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -103,3 +103,45 @@
|
||||
{"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}
|
||||
|
||||
@@ -124,3 +124,42 @@
|
||||
{"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}
|
||||
|
||||
@@ -19,3 +19,10 @@
|
||||
{"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}
|
||||
|
||||
@@ -40,3 +40,21 @@
|
||||
{"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}
|
||||
|
||||
@@ -9,7 +9,17 @@
|
||||
"podMemoryRequest": 0.5,
|
||||
"podMemoryLimit": 0.25,
|
||||
"podPhase": "running",
|
||||
"podCountsByPhase": {"pending": 0, "running": 1, "succeeded": 0, "failed": 0, "unknown": 0}
|
||||
"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
|
||||
}
|
||||
},
|
||||
{
|
||||
"podName": "acc-p2",
|
||||
@@ -20,7 +30,17 @@
|
||||
"podMemoryRequest": 0.75,
|
||||
"podMemoryLimit": 0.5,
|
||||
"podPhase": "running",
|
||||
"podCountsByPhase": {"pending": 0, "running": 1, "succeeded": 0, "failed": 0, "unknown": 0}
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
@@ -121,6 +122,9 @@ def test_pods_accuracy(
|
||||
"podMemoryLimit",
|
||||
"podPhase",
|
||||
"podCountsByPhase",
|
||||
"podStatus",
|
||||
"podCountsByStatus",
|
||||
"podRestarts",
|
||||
"podAge",
|
||||
"meta",
|
||||
):
|
||||
@@ -131,6 +135,11 @@ 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]
|
||||
@@ -145,6 +154,9 @@ 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}"
|
||||
|
||||
|
||||
@@ -793,3 +805,343 @@ 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
|
||||
|
||||
@@ -74,6 +74,7 @@ def test_namespaces_accuracy(
|
||||
"namespaceCPU",
|
||||
"namespaceMemory",
|
||||
"podCountsByPhase",
|
||||
"counts",
|
||||
"meta",
|
||||
):
|
||||
assert field in record, f"missing {field} in {record!r}"
|
||||
@@ -81,6 +82,9 @@ def test_namespaces_accuracy(
|
||||
for bucket in ("pending", "running", "succeeded", "failed", "unknown"):
|
||||
assert bucket in record["podCountsByPhase"]
|
||||
assert isinstance(record["podCountsByPhase"][bucket], int)
|
||||
for bucket in ("deployments", "daemonSets", "jobs", "statefulSets"):
|
||||
assert bucket in record["counts"]
|
||||
assert isinstance(record["counts"][bucket], int)
|
||||
|
||||
assert record["meta"].get("k8s.namespace.name") == record["namespaceName"]
|
||||
assert "k8s.cluster.name" in record["meta"]
|
||||
@@ -90,6 +94,7 @@ def test_namespaces_accuracy(
|
||||
for field in ("namespaceCPU", "namespaceMemory"):
|
||||
assert compare_values(record[field], exp[field], 1e-6), f"{record['namespaceName']}.{field}: got {record[field]}, expected {exp[field]}"
|
||||
assert record["podCountsByPhase"] == exp["podCountsByPhase"]
|
||||
assert record["counts"] == exp["counts"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
|
||||
@@ -77,6 +78,7 @@ def test_clusters_accuracy(
|
||||
"clusterMemoryAllocatable",
|
||||
"nodeCountsByReadiness",
|
||||
"podCountsByPhase",
|
||||
"counts",
|
||||
"meta",
|
||||
):
|
||||
assert field in record, f"missing {field} in {record!r}"
|
||||
@@ -87,6 +89,9 @@ def test_clusters_accuracy(
|
||||
for bucket in ("pending", "running", "succeeded", "failed", "unknown"):
|
||||
assert bucket in record["podCountsByPhase"]
|
||||
assert isinstance(record["podCountsByPhase"][bucket], int)
|
||||
for bucket in ("nodes", "namespaces", "deployments", "daemonSets", "jobs", "statefulSets"):
|
||||
assert bucket in record["counts"]
|
||||
assert isinstance(record["counts"][bucket], int)
|
||||
|
||||
assert record["meta"].get("k8s.cluster.name") == record["clusterName"]
|
||||
|
||||
@@ -101,6 +106,7 @@ def test_clusters_accuracy(
|
||||
assert compare_values(record[field], exp[field], 1e-6), f"{record['clusterName']}.{field}: got {record[field]}, expected {exp[field]}"
|
||||
assert record["nodeCountsByReadiness"] == exp["nodeCountsByReadiness"]
|
||||
assert record["podCountsByPhase"] == exp["podCountsByPhase"]
|
||||
assert record["counts"] == exp["counts"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -404,6 +410,49 @@ 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",
|
||||
[
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
|
||||
@@ -385,6 +386,49 @@ 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
|
||||
|
||||
@@ -45,6 +45,13 @@ _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": {
|
||||
@@ -58,8 +65,8 @@ SPECS = {
|
||||
"attrs": {HMR: ["process.pid"]},
|
||||
},
|
||||
"pods": {
|
||||
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase"]},
|
||||
"optional": {KSR: list(_PODS_OPT)},
|
||||
"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"]},
|
||||
"attrs": {KAP: ["k8s.pod.uid"]},
|
||||
},
|
||||
"nodes": {
|
||||
@@ -67,37 +74,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": {},
|
||||
"optional": {KCR: list(_POD_STATUS_OPT)},
|
||||
"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)},
|
||||
"optional": {KSR: list(_PODS_OPT), KCR: list(_POD_STATUS_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)},
|
||||
"optional": {KSR: list(_PODS_OPT), KCR: list(_POD_STATUS_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)},
|
||||
"optional": {KSR: list(_PODS_OPT), KCR: list(_POD_STATUS_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)},
|
||||
"optional": {KSR: list(_PODS_OPT), KCR: list(_POD_STATUS_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": {},
|
||||
"optional": {KCR: list(_POD_STATUS_OPT)},
|
||||
"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": {},
|
||||
"optional": {KCR: list(_POD_STATUS_OPT)},
|
||||
"attrs": {RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
"volumes": {
|
||||
|
||||
Reference in New Issue
Block a user