mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-07 07:00:38 +01:00
Compare commits
60 Commits
issue-5535
...
nv/dashboa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6151467b3e | ||
|
|
daa999d1d6 | ||
|
|
d4646657ca | ||
|
|
29681572db | ||
|
|
a3fa475e88 | ||
|
|
64d0ada495 | ||
|
|
60886fd906 | ||
|
|
d8954da307 | ||
|
|
7476a2cacb | ||
|
|
b400846193 | ||
|
|
f5220d078f | ||
|
|
ab750ffffd | ||
|
|
4a2b508636 | ||
|
|
33795999cf | ||
|
|
9aac37eb94 | ||
|
|
12642e27da | ||
|
|
82f8bf88fc | ||
|
|
3befc9e8c6 | ||
|
|
ee5b85f7a4 | ||
|
|
a05164f225 | ||
|
|
35e4013826 | ||
|
|
5884c6aa90 | ||
|
|
533a430714 | ||
|
|
ba6af34714 | ||
|
|
851c7b0ad7 | ||
|
|
ef5a67495c | ||
|
|
9f540ca84b | ||
|
|
40a6b22aed | ||
|
|
6f16416f27 | ||
|
|
f8aa1c1c34 | ||
|
|
65835394c0 | ||
|
|
f132b7e53a | ||
|
|
d4ae156dc4 | ||
|
|
d6bdf9c2b2 | ||
|
|
7ea654f1aa | ||
|
|
3fd7d013a1 | ||
|
|
fb921dd381 | ||
|
|
58020d9e00 | ||
|
|
7a5933e822 | ||
|
|
2533683de6 | ||
|
|
2670d53170 | ||
|
|
8943a9454b | ||
|
|
9a7ed5b711 | ||
|
|
2d75e3d32d | ||
|
|
1d6eabf927 | ||
|
|
082d7b1b77 | ||
|
|
5019dee2d7 | ||
|
|
216de973fb | ||
|
|
18c0eec5e2 | ||
|
|
2ccdeb3631 | ||
|
|
ad12e50bbc | ||
|
|
e247bf3864 | ||
|
|
f4651ea134 | ||
|
|
d449a2dbf2 | ||
|
|
d4b9f91062 | ||
|
|
530710b7bc | ||
|
|
4fb5eec08d | ||
|
|
f889d36f0f | ||
|
|
db12d44523 | ||
|
|
86fc0e81ba |
2
.github/workflows/integrationci.yaml
vendored
2
.github/workflows/integrationci.yaml
vendored
@@ -56,8 +56,6 @@ jobs:
|
||||
- querier_json_body
|
||||
- querier_skip_resource_fingerprint
|
||||
- ttl
|
||||
- clickhousecluster
|
||||
- metricreduction
|
||||
sqlstore-provider:
|
||||
- postgres
|
||||
- sqlite
|
||||
|
||||
@@ -2682,7 +2682,6 @@ components:
|
||||
unit:
|
||||
type: string
|
||||
value:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- value
|
||||
@@ -3649,7 +3648,6 @@ components:
|
||||
unit:
|
||||
type: string
|
||||
value:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- value
|
||||
@@ -3686,7 +3684,6 @@ components:
|
||||
unit:
|
||||
type: string
|
||||
value:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- value
|
||||
@@ -4261,6 +4258,8 @@ components:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeCountsByReadiness'
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
required:
|
||||
- clusterName
|
||||
- clusterCPU
|
||||
@@ -4269,6 +4268,7 @@ components:
|
||||
- clusterMemoryAllocatable
|
||||
- nodeCountsByReadiness
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesClusters:
|
||||
@@ -4324,6 +4324,8 @@ components:
|
||||
type: object
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
required:
|
||||
- daemonSetName
|
||||
- daemonSetCPU
|
||||
@@ -4335,6 +4337,7 @@ components:
|
||||
- desiredNodes
|
||||
- currentNodes
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesDaemonSets:
|
||||
@@ -4390,6 +4393,8 @@ components:
|
||||
type: object
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
required:
|
||||
- deploymentName
|
||||
- deploymentCPU
|
||||
@@ -4401,6 +4406,7 @@ components:
|
||||
- desiredPods
|
||||
- availablePods
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesDeployments:
|
||||
@@ -4533,6 +4539,8 @@ components:
|
||||
type: object
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
successfulPods:
|
||||
type: integer
|
||||
required:
|
||||
@@ -4548,6 +4556,7 @@ components:
|
||||
- failedPods
|
||||
- successfulPods
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesJobs:
|
||||
@@ -4638,11 +4647,14 @@ components:
|
||||
type: string
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
required:
|
||||
- namespaceName
|
||||
- namespaceCPU
|
||||
- namespaceMemory
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesNamespaces:
|
||||
@@ -4708,11 +4720,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 +4773,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 +4861,8 @@ components:
|
||||
type: number
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
podMemory:
|
||||
format: double
|
||||
type: number
|
||||
@@ -4799,6 +4874,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 +4891,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 +5217,8 @@ components:
|
||||
type: object
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
statefulSetCPU:
|
||||
format: double
|
||||
type: number
|
||||
@@ -5143,6 +5250,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
|
||||
|
||||
@@ -3395,7 +3395,6 @@ export interface DashboardtypesThresholdWithLabelDTO {
|
||||
unit?: string;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
value: number;
|
||||
}
|
||||
@@ -3923,7 +3922,6 @@ export interface DashboardtypesComparisonThresholdDTO {
|
||||
unit?: string;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
value: number;
|
||||
}
|
||||
@@ -4212,7 +4210,6 @@ export interface DashboardtypesTableThresholdDTO {
|
||||
unit?: string;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
value: number;
|
||||
}
|
||||
@@ -5704,6 +5701,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
|
||||
@@ -5735,6 +5807,7 @@ export interface InframonitoringtypesClusterRecordDTO {
|
||||
meta: InframonitoringtypesClusterRecordDTOMeta;
|
||||
nodeCountsByReadiness: InframonitoringtypesNodeCountsByReadinessDTO;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesResponseTypeDTO {
|
||||
@@ -5838,6 +5911,7 @@ export interface InframonitoringtypesDaemonSetRecordDTO {
|
||||
*/
|
||||
meta: InframonitoringtypesDaemonSetRecordDTOMeta;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDaemonSetsDTO {
|
||||
@@ -5915,6 +5989,7 @@ export interface InframonitoringtypesDeploymentRecordDTO {
|
||||
*/
|
||||
meta: InframonitoringtypesDeploymentRecordDTOMeta;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDeploymentsDTO {
|
||||
@@ -6081,6 +6156,7 @@ export interface InframonitoringtypesJobRecordDTO {
|
||||
*/
|
||||
meta: InframonitoringtypesJobRecordDTOMeta;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -6134,6 +6210,7 @@ export interface InframonitoringtypesNamespaceRecordDTO {
|
||||
*/
|
||||
namespaceName: string;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesNamespacesDTO {
|
||||
@@ -6200,6 +6277,7 @@ export interface InframonitoringtypesNodeRecordDTO {
|
||||
*/
|
||||
nodeName: string;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesNodesDTO {
|
||||
@@ -6237,6 +6315,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 +6362,7 @@ export interface InframonitoringtypesPodRecordDTO {
|
||||
*/
|
||||
podCPURequest: number;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
@@ -6279,6 +6379,12 @@ export interface InframonitoringtypesPodRecordDTO {
|
||||
*/
|
||||
podMemoryRequest: number;
|
||||
podPhase: InframonitoringtypesPodPhaseDTO;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
podRestarts: number;
|
||||
podStatus: InframonitoringtypesPodStatusDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6596,6 +6702,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 |
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,7 @@ func buildClusterRecords(
|
||||
metadataMap map[string]map[string]string,
|
||||
nodeConditionCountsMap map[string]nodeConditionCounts,
|
||||
podPhaseCountsMap map[string]podPhaseCounts,
|
||||
podStatusCounts map[string]podStatusCounts,
|
||||
) []inframonitoringtypes.ClusterRecord {
|
||||
metricsMap := parseFullQueryResponse(resp, groupBy)
|
||||
|
||||
@@ -69,6 +70,10 @@ func buildClusterRecords(
|
||||
}
|
||||
}
|
||||
|
||||
if podStatusCountsForGroup, ok := podStatusCounts[compositeKey]; ok {
|
||||
record.PodCountsByStatus = podStatusCountsToResponse(podStatusCountsForGroup)
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
|
||||
@@ -27,6 +27,8 @@ 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{
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -309,9 +309,19 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
return nil, err
|
||||
}
|
||||
|
||||
statusCounts, statusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
restartCounts, err := m.getPerGroupPodRestartCounts(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isPodUIDInGroupBy := isKeyInGroupByAttrs(req.GroupBy, podUIDAttrKey)
|
||||
resp.Records = buildPodRecords(isPodUIDInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, 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
|
||||
}
|
||||
@@ -392,9 +402,14 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
return nil, err
|
||||
}
|
||||
|
||||
podStatusCounts, podStatusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isNodeNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, inframonitoringtypes.NodeNameAttrKey)
|
||||
resp.Records = buildNodeRecords(isNodeNameInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCounts, podPhaseCounts)
|
||||
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
|
||||
}
|
||||
@@ -470,8 +485,13 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildNamespaceRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
podStatusCounts, podStatusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildNamespaceRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -554,8 +574,13 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildClusterRecords(queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCountsMap, podPhaseCountsMap)
|
||||
resp.Warning = queryResp.Warning
|
||||
podStatusCounts, podStatusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildClusterRecords(queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCountsMap, podPhaseCountsMap, podStatusCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -715,8 +740,13 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildDeploymentRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
podStatusCounts, podStatusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildDeploymentRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -800,8 +830,13 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildStatefulSetRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
podStatusCounts, podStatusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildStatefulSetRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -885,8 +920,13 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildJobRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
podStatusCounts, podStatusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildJobRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -970,8 +1010,13 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildDaemonSetRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
podStatusCounts, podStatusWarning, err := m.getPerGroupPodStatusCountsWithReqMetricChecks(ctx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildDaemonSetRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts, podStatusCounts)
|
||||
resp.Warning = mergeQueryWarnings(queryResp.Warning, podStatusWarning)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ func buildNamespaceRecords(
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
metadataMap map[string]map[string]string,
|
||||
phaseCounts map[string]podPhaseCounts,
|
||||
podStatusCounts map[string]podStatusCounts,
|
||||
) []inframonitoringtypes.NamespaceRecord {
|
||||
metricsMap := parseFullQueryResponse(resp, groupBy)
|
||||
|
||||
@@ -52,6 +53,10 @@ func buildNamespaceRecords(
|
||||
}
|
||||
}
|
||||
|
||||
if podStatusCountsForGroup, ok := podStatusCounts[compositeKey]; ok {
|
||||
record.PodCountsByStatus = podStatusCountsToResponse(podStatusCountsForGroup)
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -39,48 +39,48 @@ func TestReducedStatementBuilder(t *testing.T) {
|
||||
name: "gauge_sum_latest",
|
||||
query: reducedQuery("test.metric", metrictypes.GaugeType, metrictypes.Unspecified, metrictypes.TimeAggregationLatest, metrictypes.SpaceAggregationSum),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, anyLast(last) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, argMax(value, unix_milli) AS per_series_value FROM (SELECT points.reduced_fingerprint AS fingerprint, points.unix_milli AS unix_milli, argMax(`sum_last`, points.computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_last_60s AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.reduced_fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, unix_milli) GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), false, "test.metric", uint64(1746999900000), uint64(1747172760000)},
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, anyLast(last) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, argMax(value, unix_milli) AS per_series_value FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`sum_last`, computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_last_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), "test.metric", uint64(1746999900000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gauge_avg_avg",
|
||||
query: reducedQuery("test.metric", metrictypes.GaugeType, metrictypes.Unspecified, metrictypes.TimeAggregationAvg, metrictypes.SpaceAggregationAvg),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, sum(sum) / sum(count) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, avg(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, avg(value) AS per_series_value, avg(weight) AS per_series_weight FROM (SELECT points.reduced_fingerprint AS fingerprint, points.unix_milli AS unix_milli, argMax(`sum_last`, points.computed_at) AS value, argMax(`count_series`, points.computed_at) AS weight FROM signoz_metrics.distributed_samples_v4_reduced_last_60s AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.reduced_fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, unix_milli) GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) / sum(per_series_weight) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), false, "test.metric", uint64(1746999900000), uint64(1747172760000)},
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, sum(sum) / sum(count) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, avg(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, avg(value) AS per_series_value, avg(weight) AS per_series_weight FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`sum_last`, computed_at) AS value, argMax(`count_series`, computed_at) AS weight FROM signoz_metrics.distributed_samples_v4_reduced_last_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) / sum(per_series_weight) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), "test.metric", uint64(1746999900000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gauge_min_min",
|
||||
query: reducedQuery("test.metric", metrictypes.GaugeType, metrictypes.Unspecified, metrictypes.TimeAggregationMin, metrictypes.SpaceAggregationMin),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, min(min) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, min(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, min(value) AS per_series_value FROM (SELECT points.reduced_fingerprint AS fingerprint, points.unix_milli AS unix_milli, argMax(`min`, points.computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_last_60s AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.reduced_fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, unix_milli) GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, min(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), false, "test.metric", uint64(1746999900000), uint64(1747172760000)},
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, min(min) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, min(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, min(value) AS per_series_value FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`min`, computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_last_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, min(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), "test.metric", uint64(1746999900000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gauge_max_max",
|
||||
query: reducedQuery("test.metric", metrictypes.GaugeType, metrictypes.Unspecified, metrictypes.TimeAggregationMax, metrictypes.SpaceAggregationMax),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, max(max) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, max(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, max(value) AS per_series_value FROM (SELECT points.reduced_fingerprint AS fingerprint, points.unix_milli AS unix_milli, argMax(`max`, points.computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_last_60s AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.reduced_fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, unix_milli) GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, max(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), false, "test.metric", uint64(1746999900000), uint64(1747172760000)},
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, max(max) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, max(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, max(value) AS per_series_value FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`max`, computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_last_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, max(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), "test.metric", uint64(1746999900000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "counter_sum_rate",
|
||||
query: reducedQuery("test.metric.sum", metrictypes.SumType, metrictypes.Cumulative, metrictypes.TimeAggregationRate, metrictypes.SpaceAggregationSum),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT ts, multiIf(row_number() OVER rate_window = 1, nan, (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) < 0, per_series_value / (ts - lagInFrame(ts, 1) OVER rate_window), (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) / (ts - lagInFrame(ts, 1) OVER rate_window)) AS per_series_value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, max(max) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, sum(value) / 300 AS per_series_value FROM (SELECT points.reduced_fingerprint AS fingerprint, points.unix_milli AS unix_milli, argMax(`sum`, points.computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_sum_60s AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.reduced_fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, unix_milli) GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric.sum", uint64(1746921600000), uint64(1747172760000), "cumulative", false, "test.metric.sum", uint64(1746999600000), uint64(1747172760000), 0, "test.metric.sum", uint64(1746999600000), uint64(1747172760000), false, "test.metric.sum", uint64(1746999600000), uint64(1747172760000)},
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT ts, multiIf(row_number() OVER rate_window = 1, nan, (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) < 0, per_series_value / (ts - lagInFrame(ts, 1) OVER rate_window), (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) / (ts - lagInFrame(ts, 1) OVER rate_window)) AS per_series_value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, max(max) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, sum(value) / 300 AS per_series_value FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`sum`, computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_sum_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric.sum", uint64(1746921600000), uint64(1747172760000), "cumulative", false, "test.metric.sum", uint64(1746999600000), uint64(1747172760000), 0, "test.metric.sum", uint64(1746999600000), uint64(1747172760000), "test.metric.sum", uint64(1746999600000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "counter_avg_increase",
|
||||
query: reducedQuery("test.metric", metrictypes.SumType, metrictypes.Cumulative, metrictypes.TimeAggregationIncrease, metrictypes.SpaceAggregationAvg),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT ts, multiIf(row_number() OVER rate_window = 1, nan, (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) < 0, per_series_value, per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) AS per_series_value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, max(max) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, avg(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, sum(value) AS per_series_value, avg(weight) AS per_series_weight FROM (SELECT points.reduced_fingerprint AS fingerprint, points.unix_milli AS unix_milli, argMax(`sum`, points.computed_at) AS value, argMax(`count_series`, points.computed_at) AS weight FROM signoz_metrics.distributed_samples_v4_reduced_sum_60s AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.reduced_fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, unix_milli) GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) / sum(per_series_weight) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "cumulative", false, "test.metric", uint64(1746999600000), uint64(1747172760000), 0, "test.metric", uint64(1746999600000), uint64(1747172760000), false, "test.metric", uint64(1746999600000), uint64(1747172760000)},
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT ts, multiIf(row_number() OVER rate_window = 1, nan, (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) < 0, per_series_value, per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) AS per_series_value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, max(max) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, avg(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, sum(value) AS per_series_value, avg(weight) AS per_series_weight FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`sum`, computed_at) AS value, argMax(`count_series`, computed_at) AS weight FROM signoz_metrics.distributed_samples_v4_reduced_sum_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) / sum(per_series_weight) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "cumulative", false, "test.metric", uint64(1746999600000), uint64(1747172760000), 0, "test.metric", uint64(1746999600000), uint64(1747172760000), "test.metric", uint64(1746999600000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -103,16 +103,16 @@ func TestReducedStatementBuilder(t *testing.T) {
|
||||
name: "histogram_p99",
|
||||
query: reducedQuery("test.metric.bucket", metrictypes.HistogramType, metrictypes.Cumulative, metrictypes.TimeAggregationUnspecified, metrictypes.SpaceAggregationPercentile99),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT ts, `le`, multiIf(row_number() OVER rate_window = 1, nan, (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) < 0, per_series_value / (ts - lagInFrame(ts, 1) OVER rate_window), (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) / (ts - lagInFrame(ts, 1) OVER rate_window)) AS per_series_value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, `le`, max(max) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint, JSONExtractString(labels, 'le') AS `le` FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint, `le`) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts, `le` ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, `le`, sum(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts, `le`) SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) AS value FROM __spatial_aggregation_cte GROUP BY ts ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, `le`, sum(value) / 300 AS per_series_value FROM (SELECT points.reduced_fingerprint AS fingerprint, points.unix_milli AS unix_milli, `le`, argMax(`sum`, points.computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_sum_60s AS points INNER JOIN (SELECT fingerprint, JSONExtractString(labels, 'le') AS `le` FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint, `le`) AS filtered_time_series ON points.reduced_fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, unix_milli, `le`) GROUP BY fingerprint, ts, `le`), __spatial_aggregation_cte AS (SELECT ts, `le`, sum(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts, `le`) SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) AS value FROM __spatial_aggregation_cte GROUP BY ts ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric.bucket", uint64(1746921600000), uint64(1747172760000), "cumulative", false, "test.metric.bucket", uint64(1746999900000), uint64(1747172760000), 0, "test.metric.bucket", uint64(1746999900000), uint64(1747172760000), false, "test.metric.bucket", uint64(1746999900000), uint64(1747172760000)},
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT ts, `le`, multiIf(row_number() OVER rate_window = 1, nan, (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) < 0, per_series_value / (ts - lagInFrame(ts, 1) OVER rate_window), (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) / (ts - lagInFrame(ts, 1) OVER rate_window)) AS per_series_value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, `le`, max(max) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint, JSONExtractString(labels, 'le') AS `le` FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint, `le`) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts, `le` ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, `le`, sum(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts, `le`) SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) AS value FROM __spatial_aggregation_cte GROUP BY ts ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, `le`, sum(value) / 300 AS per_series_value FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`sum`, computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_sum_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint, JSONExtractString(labels, 'le') AS `le` FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint, `le`) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts, `le`), __spatial_aggregation_cte AS (SELECT ts, `le`, sum(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts, `le`) SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) AS value FROM __spatial_aggregation_cte GROUP BY ts ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric.bucket", uint64(1746921600000), uint64(1747172760000), "cumulative", false, "test.metric.bucket", uint64(1746999900000), uint64(1747172760000), 0, "test.metric.bucket", uint64(1746999900000), uint64(1747172760000), "test.metric.bucket", uint64(1746999900000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "summary_avg",
|
||||
query: reducedQuery("test.metric", metrictypes.SummaryType, metrictypes.Unspecified, metrictypes.TimeAggregationAvg, metrictypes.SpaceAggregationAvg),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, sum(sum) / sum(count) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, avg(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, avg(value) AS per_series_value, avg(weight) AS per_series_weight FROM (SELECT points.reduced_fingerprint AS fingerprint, points.unix_milli AS unix_milli, argMax(`sum_last`, points.computed_at) AS value, argMax(`count_series`, points.computed_at) AS weight FROM signoz_metrics.distributed_samples_v4_reduced_last_60s AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.reduced_fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, unix_milli) GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) / sum(per_series_weight) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), false, "test.metric", uint64(1746999900000), uint64(1747172760000)},
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, sum(sum) / sum(count) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, avg(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, avg(value) AS per_series_value, avg(weight) AS per_series_weight FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`sum_last`, computed_at) AS value, argMax(`count_series`, computed_at) AS weight FROM signoz_metrics.distributed_samples_v4_reduced_last_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) / sum(per_series_weight) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), "test.metric", uint64(1746999900000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -337,28 +337,20 @@ func (b *MetricQueryStatementBuilder) buildReducedTemporalAggregationCTE(
|
||||
}
|
||||
|
||||
// dedup recomputed buckets: latest computed_at wins per (series, 60s bucket)
|
||||
// TODO(srikanthccv): add _5m/_30m tables similar to samples_v4
|
||||
// and wrie them up in querier before GA
|
||||
// TODO(srikanthccv): FINAL clause for the reduced table.
|
||||
dedup := sqlbuilder.NewSelectBuilder()
|
||||
dedup.Select("points.reduced_fingerprint AS fingerprint", "points.unix_milli AS unix_milli")
|
||||
for _, g := range query.GroupBy {
|
||||
dedup.SelectMore(fmt.Sprintf("`%s`", g.Name))
|
||||
}
|
||||
dedup.SelectMore(fmt.Sprintf("argMax(%s, points.computed_at) AS value", value))
|
||||
dedup.Select("reduced_fingerprint AS fingerprint", "unix_milli")
|
||||
dedup.SelectMore(fmt.Sprintf("argMax(%s, computed_at) AS value", value))
|
||||
if weight != "" {
|
||||
dedup.SelectMore(fmt.Sprintf("argMax(%s, points.computed_at) AS weight", weight))
|
||||
dedup.SelectMore(fmt.Sprintf("argMax(%s, computed_at) AS weight", weight))
|
||||
}
|
||||
dedup.From(fmt.Sprintf("%s.%s AS points", DBName, WhichReducedSamplesTableToUse(agg.Type)))
|
||||
dedup.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.reduced_fingerprint = filtered_time_series.fingerprint")
|
||||
dedup.From(fmt.Sprintf("%s.%s", DBName, WhichReducedSamplesTableToUse(agg.Type)))
|
||||
dedup.Where(
|
||||
dedup.In("metric_name", agg.MetricName),
|
||||
dedup.GTE("unix_milli", start),
|
||||
dedup.LT("unix_milli", end),
|
||||
)
|
||||
dedup.GroupBy("fingerprint", "unix_milli")
|
||||
dedup.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
|
||||
dedupQuery, dedupArgs := dedup.BuildWithFlavor(sqlbuilder.ClickHouse, timeSeriesCTEArgs...)
|
||||
dedup.GroupBy("reduced_fingerprint", "unix_milli")
|
||||
dedupQuery, dedupArgs := dedup.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("fingerprint")
|
||||
@@ -372,11 +364,13 @@ func (b *MetricQueryStatementBuilder) buildReducedTemporalAggregationCTE(
|
||||
// denominator is reduced with avg
|
||||
sb.SelectMore("avg(weight) AS per_series_weight")
|
||||
}
|
||||
sb.From(fmt.Sprintf("(%s)", dedupQuery))
|
||||
sb.From(fmt.Sprintf("(%s) AS points", dedupQuery))
|
||||
sb.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
|
||||
sb.GroupBy("fingerprint", "ts")
|
||||
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
|
||||
|
||||
q, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse, dedupArgs...)
|
||||
initArgs := append(append([]any{}, dedupArgs...), timeSeriesCTEArgs...)
|
||||
q, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse, initArgs...)
|
||||
return fmt.Sprintf("__temporal_aggregation_cte AS (%s)", q), args, true
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/telemetrytraces"
|
||||
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
type migrateCommon struct {
|
||||
@@ -23,119 +24,10 @@ func NewMigrateCommon(logger *slog.Logger) *migrateCommon {
|
||||
}
|
||||
}
|
||||
|
||||
// WrapInV5Envelope delegates to querybuildertypesv5.WrapInV5Envelope; the
|
||||
// transform is stateless and shared with the v1→v2 dashboard conversion.
|
||||
func (migration *migrateCommon) WrapInV5Envelope(name string, queryMap map[string]any, queryType string) map[string]any {
|
||||
// Create a properly structured v5 query
|
||||
v5Query := map[string]any{
|
||||
"name": name,
|
||||
"disabled": queryMap["disabled"],
|
||||
"legend": queryMap["legend"],
|
||||
}
|
||||
|
||||
if name != queryMap["expression"] {
|
||||
// formula
|
||||
queryType = "builder_formula"
|
||||
v5Query["expression"] = queryMap["expression"]
|
||||
if functions, ok := queryMap["functions"]; ok {
|
||||
v5Query["functions"] = functions
|
||||
}
|
||||
return map[string]any{
|
||||
"type": queryType,
|
||||
"spec": v5Query,
|
||||
}
|
||||
}
|
||||
|
||||
// Add signal based on data source
|
||||
if dataSource, ok := queryMap["dataSource"].(string); ok {
|
||||
switch dataSource {
|
||||
case "traces":
|
||||
v5Query["signal"] = "traces"
|
||||
case "logs":
|
||||
v5Query["signal"] = "logs"
|
||||
case "metrics":
|
||||
v5Query["signal"] = "metrics"
|
||||
}
|
||||
}
|
||||
|
||||
if stepInterval, ok := queryMap["stepInterval"]; ok {
|
||||
v5Query["stepInterval"] = stepInterval
|
||||
}
|
||||
|
||||
if aggregations, ok := queryMap["aggregations"]; ok {
|
||||
v5Query["aggregations"] = aggregations
|
||||
}
|
||||
|
||||
if filter, ok := queryMap["filter"]; ok {
|
||||
v5Query["filter"] = filter
|
||||
}
|
||||
|
||||
// Copy groupBy with proper structure
|
||||
if groupBy, ok := queryMap["groupBy"].([]any); ok {
|
||||
v5GroupBy := make([]any, len(groupBy))
|
||||
for i, gb := range groupBy {
|
||||
if gbMap, ok := gb.(map[string]any); ok {
|
||||
v5GroupBy[i] = map[string]any{
|
||||
"name": gbMap["key"],
|
||||
"fieldDataType": gbMap["dataType"],
|
||||
"fieldContext": gbMap["type"],
|
||||
}
|
||||
}
|
||||
}
|
||||
v5Query["groupBy"] = v5GroupBy
|
||||
}
|
||||
|
||||
// Copy orderBy with proper structure
|
||||
if orderBy, ok := queryMap["orderBy"].([]any); ok {
|
||||
v5OrderBy := make([]any, len(orderBy))
|
||||
for i, ob := range orderBy {
|
||||
if obMap, ok := ob.(map[string]any); ok {
|
||||
v5OrderBy[i] = map[string]any{
|
||||
"key": map[string]any{
|
||||
"name": obMap["columnName"],
|
||||
"fieldDataType": obMap["dataType"],
|
||||
"fieldContext": obMap["type"],
|
||||
},
|
||||
"direction": obMap["order"],
|
||||
}
|
||||
}
|
||||
}
|
||||
v5Query["order"] = v5OrderBy
|
||||
}
|
||||
|
||||
// Copy selectColumns as selectFields
|
||||
if selectColumns, ok := queryMap["selectColumns"].([]any); ok {
|
||||
v5SelectFields := make([]any, len(selectColumns))
|
||||
for i, col := range selectColumns {
|
||||
if colMap, ok := col.(map[string]any); ok {
|
||||
v5SelectFields[i] = map[string]any{
|
||||
"name": colMap["key"],
|
||||
"fieldDataType": colMap["dataType"],
|
||||
"fieldContext": colMap["type"],
|
||||
}
|
||||
}
|
||||
}
|
||||
v5Query["selectFields"] = v5SelectFields
|
||||
}
|
||||
|
||||
// Copy limit and offset
|
||||
if limit, ok := queryMap["limit"]; ok {
|
||||
v5Query["limit"] = limit
|
||||
}
|
||||
if offset, ok := queryMap["offset"]; ok {
|
||||
v5Query["offset"] = offset
|
||||
}
|
||||
|
||||
if having, ok := queryMap["having"]; ok {
|
||||
v5Query["having"] = having
|
||||
}
|
||||
|
||||
if functions, ok := queryMap["functions"]; ok {
|
||||
v5Query["functions"] = functions
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"type": queryType,
|
||||
"spec": v5Query,
|
||||
}
|
||||
return querybuildertypesv5.WrapInV5Envelope(name, queryMap, queryType)
|
||||
}
|
||||
|
||||
func (mc *migrateCommon) updateQueryData(ctx context.Context, queryData map[string]any, version, widgetType string) bool {
|
||||
|
||||
353
pkg/transition/migrate_shape_safe.go
Normal file
353
pkg/transition/migrate_shape_safe.go
Normal file
@@ -0,0 +1,353 @@
|
||||
// nolint
|
||||
package transition
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Shape-safe (idempotent) migration
|
||||
// ══════════════════════════════════════════════
|
||||
//
|
||||
// A copy of the Migrate → updateWidget → updateQueryData chain with the
|
||||
// "uniformly v4 input" assumption removed, so it is safe on a dashboard whose
|
||||
// `version` tag lies (a "v5"-labelled dashboard with un-upgraded, possibly mixed,
|
||||
// bodies — the v1→v2 converter's case). Versus the original: no version gate, and
|
||||
// each step acts only on the pre-v5 shape (leaving a v5 field alone), so it is
|
||||
// idempotent. The original Migrate is left unchanged (battle-tested, no test net).
|
||||
// The *ShapeSafe methods below each note the original they copy; the reused steps
|
||||
// (createFilterExpression, fixGroupBy, buildAggregationExpression, orderByExpr) are
|
||||
// already v5-safe.
|
||||
|
||||
// MigrateQueryDataShapeSafe is the per-query entry point (the core of
|
||||
// updateQueryDataShapeSafe) for callers that process queries one at a time (the
|
||||
// v1→v2 converter). widgetType is the v1 panelTypes (metric reduceTo on tables);
|
||||
// "" is safe.
|
||||
func (m *dashboardMigrateV5) MigrateQueryDataShapeSafe(ctx context.Context, queryData map[string]any, widgetType string) bool {
|
||||
return m.updateQueryDataShapeSafe(ctx, queryData, widgetType)
|
||||
}
|
||||
|
||||
// updateQueryDataShapeSafe copies updateQueryData, with each destructive step
|
||||
// guarded to act only on the pre-v5 shape (see the file header).
|
||||
func (mc *migrateCommon) updateQueryDataShapeSafe(ctx context.Context, queryData map[string]any, widgetType string) bool {
|
||||
updated := false
|
||||
|
||||
aggregateOp, _ := queryData["aggregateOperator"].(string)
|
||||
hasAggregation := aggregateOp != "" && aggregateOp != "noop"
|
||||
|
||||
if mc.createAggregationsShapeSafe(ctx, queryData, widgetType) {
|
||||
updated = true
|
||||
}
|
||||
|
||||
// createFilterExpression only touches v4 `filters`; skip if a v5 `filter` exists.
|
||||
if _, hasFilter := queryData["filter"]; !hasFilter {
|
||||
if mc.createFilterExpression(ctx, queryData) {
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
if mc.fixGroupBy(queryData) {
|
||||
updated = true
|
||||
}
|
||||
|
||||
if mc.createHavingExpressionShapeSafe(queryData) {
|
||||
updated = true
|
||||
}
|
||||
|
||||
if hasAggregation {
|
||||
if orderBy, ok := queryData["orderBy"].([]any); ok && orderByIsPreV5(orderBy) {
|
||||
newOrderBy := make([]any, 0)
|
||||
for _, order := range orderBy {
|
||||
if orderMap, ok := order.(map[string]any); ok {
|
||||
columnName, _ := orderMap["columnName"].(string)
|
||||
// skip timestamp, id (logs, traces), samples(metrics) ordering for aggregation queries
|
||||
if columnName != "timestamp" && columnName != "samples" && columnName != "id" {
|
||||
if columnName == "#SIGNOZ_VALUE" {
|
||||
if expr, has := mc.orderByExpr(queryData); has {
|
||||
orderMap["columnName"] = expr
|
||||
}
|
||||
} else {
|
||||
// if the order by key is not part of the group by keys, remove it
|
||||
present := false
|
||||
|
||||
groupBy, ok := queryData["groupBy"].([]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for idx := range groupBy {
|
||||
item, ok := groupBy[idx].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key, ok := item["key"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if key == columnName {
|
||||
present = true
|
||||
}
|
||||
}
|
||||
|
||||
if !present {
|
||||
mc.logger.WarnContext(ctx, "found a order by without group by, skipping", slog.String("order_col_name", columnName))
|
||||
continue
|
||||
}
|
||||
}
|
||||
newOrderBy = append(newOrderBy, orderMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
queryData["orderBy"] = newOrderBy
|
||||
updated = true
|
||||
}
|
||||
} else {
|
||||
dataSource, _ := queryData["dataSource"].(string)
|
||||
|
||||
if orderBy, ok := queryData["orderBy"].([]any); ok && orderByIsPreV5(orderBy) {
|
||||
newOrderBy := make([]any, 0)
|
||||
for _, order := range orderBy {
|
||||
if orderMap, ok := order.(map[string]any); ok {
|
||||
columnName, _ := orderMap["columnName"].(string)
|
||||
// skip id and timestamp for (traces)
|
||||
if (columnName == "id" || columnName == "timestamp") && dataSource == "traces" {
|
||||
mc.logger.InfoContext(ctx, "skipping `id` order by for traces")
|
||||
continue
|
||||
}
|
||||
|
||||
// skip id for (logs)
|
||||
if (columnName == "id" || columnName == "timestamp") && dataSource == "logs" {
|
||||
mc.logger.InfoContext(ctx, "skipping `id`/`timestamp` order by for logs")
|
||||
continue
|
||||
}
|
||||
|
||||
newOrderBy = append(newOrderBy, orderMap)
|
||||
}
|
||||
}
|
||||
queryData["orderBy"] = newOrderBy
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
// Only the `&& functionsArePreV5(functions)` guard differs from updateQueryData.
|
||||
if functions, ok := queryData["functions"].([]any); ok && functionsArePreV5(functions) {
|
||||
v5Functions := make([]any, len(functions))
|
||||
for i, fn := range functions {
|
||||
if fnMap, ok := fn.(map[string]any); ok {
|
||||
v5Function := map[string]any{
|
||||
"name": fnMap["name"],
|
||||
}
|
||||
|
||||
// Convert args from v4 format to v5 FunctionArg format
|
||||
if args, ok := fnMap["args"].([]any); ok {
|
||||
v5Args := make([]any, len(args))
|
||||
for j, arg := range args {
|
||||
// In v4, args were just values. In v5, they are FunctionArg objects
|
||||
v5Args[j] = map[string]any{
|
||||
"name": "", // v4 didn't have named args
|
||||
"value": arg,
|
||||
}
|
||||
}
|
||||
v5Function["args"] = v5Args
|
||||
}
|
||||
|
||||
// Handle namedArgs if present (some functions might have used this)
|
||||
if namedArgs, ok := fnMap["namedArgs"].(map[string]any); ok {
|
||||
// Convert named args to the new format
|
||||
existingArgs, _ := v5Function["args"].([]any)
|
||||
if existingArgs == nil {
|
||||
existingArgs = []any{}
|
||||
}
|
||||
|
||||
for name, value := range namedArgs {
|
||||
existingArgs = append(existingArgs, map[string]any{
|
||||
"name": name,
|
||||
"value": value,
|
||||
})
|
||||
}
|
||||
v5Function["args"] = existingArgs
|
||||
}
|
||||
|
||||
v5Functions[i] = v5Function
|
||||
}
|
||||
}
|
||||
queryData["functions"] = v5Functions
|
||||
updated = true
|
||||
}
|
||||
|
||||
delete(queryData, "aggregateOperator")
|
||||
delete(queryData, "aggregateAttribute")
|
||||
delete(queryData, "temporality")
|
||||
delete(queryData, "timeAggregation")
|
||||
delete(queryData, "spaceAggregation")
|
||||
delete(queryData, "reduceTo")
|
||||
delete(queryData, "filters")
|
||||
delete(queryData, "ShiftBy")
|
||||
delete(queryData, "IsAnomaly")
|
||||
delete(queryData, "QueriesUsedInFormula")
|
||||
delete(queryData, "seriesAggregation")
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
// createHavingExpressionShapeSafe copies createHavingExpression but leaves an
|
||||
// already-v5 having:{expression} alone instead of wiping it.
|
||||
func (mc *migrateCommon) createHavingExpressionShapeSafe(queryData map[string]any) bool {
|
||||
if _, ok := queryData["having"].(map[string]any); ok {
|
||||
return false // already v5-shaped
|
||||
}
|
||||
having, ok := queryData["having"].([]any)
|
||||
if !ok || len(having) == 0 {
|
||||
queryData["having"] = map[string]any{"expression": ""}
|
||||
return true
|
||||
}
|
||||
|
||||
dataSource, _ := queryData["dataSource"].(string)
|
||||
|
||||
for idx := range having {
|
||||
if havingItem, ok := having[idx].(map[string]any); ok {
|
||||
havingCol, has := mc.orderByExpr(queryData)
|
||||
if has {
|
||||
havingItem["columnName"] = havingCol
|
||||
havingItem["key"] = map[string]any{"key": havingCol}
|
||||
}
|
||||
having[idx] = havingItem
|
||||
}
|
||||
}
|
||||
queryData["having"] = map[string]any{"expression": mc.buildExpression(context.Background(), having, "AND", dataSource)}
|
||||
return true
|
||||
}
|
||||
|
||||
// createAggregationsShapeSafe copies createAggregations but skips a query that
|
||||
// already has a v5 aggregations[], and picks the metric time/space aggregation
|
||||
// from the body's shape (has timeAggregation/spaceAggregation?) rather than the
|
||||
// version tag.
|
||||
func (mc *migrateCommon) createAggregationsShapeSafe(ctx context.Context, queryData map[string]any, widgetType string) bool {
|
||||
if aggs, ok := queryData["aggregations"].([]any); ok && len(aggs) > 0 {
|
||||
return false // already v5-shaped
|
||||
}
|
||||
|
||||
aggregateOp, hasOp := queryData["aggregateOperator"].(string)
|
||||
aggregateAttr, hasAttr := queryData["aggregateAttribute"].(map[string]any)
|
||||
dataSource, _ := queryData["dataSource"].(string)
|
||||
|
||||
if aggregateOp == "noop" && dataSource != "metrics" {
|
||||
return false
|
||||
}
|
||||
if !hasOp || !hasAttr {
|
||||
return false
|
||||
}
|
||||
|
||||
var aggregation map[string]any
|
||||
|
||||
switch dataSource {
|
||||
case "metrics":
|
||||
_, hasTime := queryData["timeAggregation"]
|
||||
_, hasSpace := queryData["spaceAggregation"]
|
||||
if hasTime || hasSpace { // acts as a check for v4 shape: the body carries its own time/space aggregation.
|
||||
if _, ok := queryData["spaceAggregation"]; !ok {
|
||||
queryData["spaceAggregation"] = aggregateOp
|
||||
}
|
||||
aggregation = map[string]any{
|
||||
"metricName": aggregateAttr["key"],
|
||||
"temporality": queryData["temporality"],
|
||||
"timeAggregation": queryData["timeAggregation"],
|
||||
"spaceAggregation": queryData["spaceAggregation"],
|
||||
}
|
||||
if reduceTo, ok := queryData["reduceTo"].(string); ok {
|
||||
aggregation["reduceTo"] = reduceTo
|
||||
}
|
||||
} else {
|
||||
// v3 shape: derive time/space from the compound operator.
|
||||
var timeAgg, spaceAgg, reduceTo string
|
||||
switch aggregateOp {
|
||||
case "sum_rate", "rate_sum":
|
||||
timeAgg, spaceAgg, reduceTo = "rate", "sum", "sum"
|
||||
case "avg_rate", "rate_avg":
|
||||
timeAgg, spaceAgg, reduceTo = "rate", "avg", "avg"
|
||||
case "min_rate", "rate_min":
|
||||
timeAgg, spaceAgg, reduceTo = "rate", "min", "min"
|
||||
case "max_rate", "rate_max":
|
||||
timeAgg, spaceAgg, reduceTo = "rate", "max", "max"
|
||||
case "hist_quantile_50":
|
||||
timeAgg, spaceAgg, reduceTo = "", "p50", "avg"
|
||||
case "hist_quantile_75":
|
||||
timeAgg, spaceAgg, reduceTo = "", "p75", "avg"
|
||||
case "hist_quantile_90":
|
||||
timeAgg, spaceAgg, reduceTo = "", "p90", "avg"
|
||||
case "hist_quantile_95":
|
||||
timeAgg, spaceAgg, reduceTo = "", "p95", "avg"
|
||||
case "hist_quantile_99":
|
||||
timeAgg, spaceAgg, reduceTo = "", "p99", "avg"
|
||||
case "rate":
|
||||
timeAgg, spaceAgg, reduceTo = "rate", "sum", "sum"
|
||||
case "p99", "p90", "p75", "p50", "p25", "p20", "p10", "p05":
|
||||
mc.logger.InfoContext(ctx, "found invalid config")
|
||||
timeAgg, spaceAgg, reduceTo = "avg", "avg", "avg"
|
||||
case "min":
|
||||
timeAgg, spaceAgg, reduceTo = "min", "min", "min"
|
||||
case "max":
|
||||
timeAgg, spaceAgg, reduceTo = "max", "max", "max"
|
||||
case "avg":
|
||||
timeAgg, spaceAgg, reduceTo = "avg", "avg", "avg"
|
||||
case "sum":
|
||||
timeAgg, spaceAgg, reduceTo = "sum", "sum", "sum"
|
||||
case "count":
|
||||
timeAgg, spaceAgg, reduceTo = "count", "sum", "sum"
|
||||
case "count_distinct":
|
||||
timeAgg, spaceAgg, reduceTo = "count_distinct", "sum", "sum"
|
||||
case "noop":
|
||||
mc.logger.WarnContext(ctx, "noop found in the aggregation data")
|
||||
timeAgg, spaceAgg, reduceTo = "max", "max", "max"
|
||||
}
|
||||
aggregation = map[string]any{
|
||||
"metricName": aggregateAttr["key"],
|
||||
"temporality": queryData["temporality"],
|
||||
"timeAggregation": timeAgg,
|
||||
"spaceAggregation": spaceAgg,
|
||||
}
|
||||
if widgetType == "table" {
|
||||
aggregation["reduceTo"] = reduceTo
|
||||
} else if reduceTo, ok := queryData["reduceTo"].(string); ok {
|
||||
aggregation["reduceTo"] = reduceTo
|
||||
}
|
||||
}
|
||||
case "logs", "traces":
|
||||
aggregation = map[string]any{"expression": mc.buildAggregationExpression(aggregateOp, aggregateAttr)}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
queryData["aggregations"] = []any{aggregation}
|
||||
return true
|
||||
}
|
||||
|
||||
// orderByIsPreV5 reports whether an orderBy slice is still in the v4 shape (an
|
||||
// entry carries "columnName"); a v5 orderBy uses {key:{name}, direction}.
|
||||
func orderByIsPreV5(orderBy []any) bool {
|
||||
for _, o := range orderBy {
|
||||
if m, ok := o.(map[string]any); ok {
|
||||
if _, has := m["columnName"]; has {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// functionsArePreV5 reports whether a functions slice is still in the v4 shape
|
||||
// (args are raw values); a v5 function's args are {name,value} objects.
|
||||
func functionsArePreV5(functions []any) bool {
|
||||
for _, f := range functions {
|
||||
if m, ok := f.(map[string]any); ok {
|
||||
args, ok := m["args"].([]any)
|
||||
if !ok || len(args) == 0 {
|
||||
continue
|
||||
}
|
||||
_, argIsObject := args[0].(map[string]any)
|
||||
return !argIsObject
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/transition"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -22,6 +21,7 @@ var (
|
||||
ErrCodeDashboardInvalidSource = errors.MustNewCode("dashboard_invalid_source")
|
||||
ErrCodeDashboardImmutable = errors.MustNewCode("dashboard_immutable")
|
||||
ErrCodeDashboardInvalidPatch = errors.MustNewCode("dashboard_invalid_patch")
|
||||
ErrCodeDashboardMigrationFailed = errors.MustNewCode("dashboard_migration_failed")
|
||||
)
|
||||
|
||||
type StorableDashboard struct {
|
||||
@@ -406,27 +406,26 @@ func (dashboard *Dashboard) GetWidgetQuery(startTime, endTime, widgetIndex uint6
|
||||
widgetData := data.Widgets[widgetIndex]
|
||||
switch widgetData.Query.QueryType {
|
||||
case "builder":
|
||||
migrate := transition.NewMigrateCommon(logger)
|
||||
for _, query := range widgetData.Query.Builder.QueryData {
|
||||
queryName, ok := query["queryName"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string")
|
||||
}
|
||||
compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_query"))
|
||||
compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_query"))
|
||||
}
|
||||
for _, query := range widgetData.Query.Builder.QueryFormulas {
|
||||
queryName, ok := query["queryName"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string")
|
||||
}
|
||||
compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_formula"))
|
||||
compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_formula"))
|
||||
}
|
||||
for _, query := range widgetData.Query.Builder.QueryTraceOperator {
|
||||
queryName, ok := query["queryName"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string")
|
||||
}
|
||||
compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_trace_operator"))
|
||||
compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_trace_operator"))
|
||||
}
|
||||
case "clickhouse_sql":
|
||||
for _, query := range widgetData.Query.ClickhouseSQL {
|
||||
|
||||
@@ -1060,6 +1060,34 @@ func TestValidateRequiredFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestThresholdZeroValueAcceptedMissingRejected documents the *float64 Value:
|
||||
// a threshold at 0 (or 0.0) is valid, because the pointer lets validate:"required"
|
||||
// tell a present zero (non-nil) from an absent value (nil) — while a genuinely
|
||||
// missing value is still rejected.
|
||||
func TestThresholdZeroValueAcceptedMissingRejected(t *testing.T) {
|
||||
numberPanel := func(thresholdSpec string) string {
|
||||
return `{
|
||||
"panels": {"p1": {"kind": "Panel", "spec": {
|
||||
"plugin": {"kind": "signoz/NumberPanel", "spec": {"thresholds": [` + thresholdSpec + `]}},
|
||||
"queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
|
||||
}}},
|
||||
"layouts": []
|
||||
}`
|
||||
}
|
||||
|
||||
_, errZero := unmarshalDashboard([]byte(numberPanel(`{"value": 0, "operator": "above", "format": "text", "color": "Red"}`)))
|
||||
require.NoError(t, errZero, `a threshold "value": 0 is valid`)
|
||||
|
||||
// "value": 0.0 is the same float64 zero as "value": 0 — JSON has one number
|
||||
// type — and is accepted identically.
|
||||
_, errZeroFloat := unmarshalDashboard([]byte(numberPanel(`{"value": 0.0, "operator": "above", "format": "text", "color": "Red"}`)))
|
||||
require.NoError(t, errZeroFloat, `"value": 0.0 is the same valid zero`)
|
||||
|
||||
_, errMissing := unmarshalDashboard([]byte(numberPanel(`{"operator": "above", "format": "text", "color": "Red"}`)))
|
||||
require.Error(t, errMissing, "a genuinely missing value is still rejected")
|
||||
require.Contains(t, errMissing.Error(), "Value")
|
||||
}
|
||||
|
||||
func TestTimeSeriesPanelDefaults(t *testing.T) {
|
||||
data := []byte(`{
|
||||
"panels": {
|
||||
|
||||
@@ -251,14 +251,20 @@ type Legend struct {
|
||||
}
|
||||
|
||||
type ThresholdWithLabel struct {
|
||||
Value float64 `json:"value" validate:"required" required:"true"`
|
||||
Unit string `json:"unit"`
|
||||
Color string `json:"color" validate:"required" required:"true"`
|
||||
Label string `json:"label"`
|
||||
// Value is a pointer so a threshold at 0 is valid: validate:"required" treats
|
||||
// the float64 zero as "missing", but a non-nil *float64 to 0 passes (and nil
|
||||
// still fails, so a genuinely absent value is still rejected). nullable:"false"
|
||||
// keeps it a plain required number in the schema — it is never null in valid
|
||||
// data (validation rejects nil), so the pointer must not leak as `number|null`.
|
||||
Value *float64 `json:"value" validate:"required" required:"true" nullable:"false"`
|
||||
Unit string `json:"unit"`
|
||||
Color string `json:"color" validate:"required" required:"true"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type ComparisonThreshold struct {
|
||||
Value float64 `json:"value" validate:"required" required:"true"`
|
||||
// Value is a pointer so a threshold at 0 is valid (see ThresholdWithLabel.Value).
|
||||
Value *float64 `json:"value" validate:"required" required:"true" nullable:"false"`
|
||||
Operator ComparisonOperator `json:"operator"`
|
||||
Unit string `json:"unit"`
|
||||
Color string `json:"color" validate:"required" required:"true"`
|
||||
|
||||
94
pkg/types/dashboardtypes/perses_v1_to_v2.go
Normal file
94
pkg/types/dashboardtypes/perses_v1_to_v2.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
// V1 → V2 migration. The v1 storable shape is the frontend's `DashboardData`
|
||||
// (see frontend/src/types/api/dashboard/getAll.ts); v2 is DashboardV2 /
|
||||
// DashboardSpec.
|
||||
//
|
||||
// Assumes the v1 widget query data has already been migrated to v5 shape
|
||||
// (transition.dashboardMigrateV5). Pre-v5 builder queries will produce
|
||||
// invalid v2 envelopes — run the v4→v5 migration first.
|
||||
//
|
||||
// The conversion is split across sibling files by concern:
|
||||
// - perses_v1_to_v2_tags.go tags
|
||||
// - perses_v1_to_v2_panels.go widgets → panels (+ panel field mappers)
|
||||
// - perses_v1_to_v2_queries.go widget queries
|
||||
// - perses_v1_to_v2_layouts.go grid layouts and sections
|
||||
// - perses_v1_to_v2_variables.go variables
|
||||
// - perses_v1_to_v2_decoder.go v1Decoder: typed field reads + malformed-field detection
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Entry point
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
func (storable StorableDashboard) IsV2() bool {
|
||||
metadata, _ := storable.Data["metadata"].(map[string]any)
|
||||
if metadata == nil {
|
||||
return false
|
||||
}
|
||||
version, _ := metadata["schemaVersion"].(string)
|
||||
return version == SchemaVersion
|
||||
}
|
||||
|
||||
func (storable StorableDashboard) ConvertV1ToV2() (result *DashboardV2, err error) {
|
||||
// Legacy v1 data can be arbitrarily malformed. The accessors degrade
|
||||
// gracefully, but recover from any unforeseen panic so one bad dashboard
|
||||
// surfaces as an error (to be logged and skipped) rather than crashing the run.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
result, err = nil, errors.Newf(errors.TypeInternal, ErrCodeDashboardMigrationFailed, "panic converting dashboard %s: %v", storable.ID, r)
|
||||
}
|
||||
}()
|
||||
|
||||
if storable.IsV2() {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardMigrationFailed, "dashboard %s is already in %s schema", storable.ID, SchemaVersion)
|
||||
}
|
||||
|
||||
d := &v1Decoder{}
|
||||
title := d.readString(storable.Data, "title")
|
||||
description := d.readString(storable.Data, "description")
|
||||
image := d.readString(storable.Data, "image")
|
||||
|
||||
panels := d.convertV1Panels(storable.Data["widgets"])
|
||||
spec := DashboardSpec{
|
||||
Display: Display{Name: title, Description: description},
|
||||
Variables: d.convertV1Variables(storable.Data["variables"]),
|
||||
Panels: panels,
|
||||
Layouts: d.convertV1Layouts(storable.Data, panels),
|
||||
}
|
||||
|
||||
// marshal and unmarshal cycle to confirm full validation
|
||||
raw, marshalErr := json.Marshal(spec)
|
||||
if marshalErr != nil {
|
||||
return nil, errors.WrapInternalf(marshalErr, errors.CodeInternal, "marshal converted dashboard %s", storable.ID)
|
||||
}
|
||||
if err := json.Unmarshal(raw, new(DashboardSpec)); err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, ErrCodeDashboardMigrationFailed, "converted dashboard %s is invalid", storable.ID)
|
||||
}
|
||||
tags := d.convertV1TagsForOrg(storable.OrgID, storable.Data["tags"])
|
||||
|
||||
if err := d.errIfHasMalformedFields(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DashboardV2{
|
||||
Identifiable: storable.Identifiable,
|
||||
TimeAuditable: storable.TimeAuditable,
|
||||
UserAuditable: storable.UserAuditable,
|
||||
OrgID: storable.OrgID,
|
||||
Locked: storable.Locked,
|
||||
Source: storable.Source,
|
||||
DashboardV2MetadataBase: DashboardV2MetadataBase{
|
||||
SchemaVersion: SchemaVersion,
|
||||
Image: image,
|
||||
},
|
||||
Name: generateDashboardName(title),
|
||||
Tags: tags,
|
||||
Spec: spec,
|
||||
}, nil
|
||||
}
|
||||
168
pkg/types/dashboardtypes/perses_v1_to_v2_decoder.go
Normal file
168
pkg/types/dashboardtypes/perses_v1_to_v2_decoder.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// v1 decoder
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// v1Decoder reads fields out of the untyped v1 dashboard blob. Every read*
|
||||
// method follows the same contract: a field that is absent or null yields the
|
||||
// zero value; a field present with the wrong type yields zero AND records a
|
||||
// malformed-field error. Conversion proceeds (so one bad field doesn't abort
|
||||
// the rest) and ConvertV1ToV2 returns d.malformedFieldsErr() at the end so the
|
||||
// dashboard is logged and skipped.
|
||||
//
|
||||
// Polymorphic v1 fields (spanGaps bool|number, selectedValue string|array, …)
|
||||
// are read with a type switch on the already-extracted value, never through
|
||||
// these accessors, so they stay lenient by construction.
|
||||
type v1Decoder struct {
|
||||
bad []string
|
||||
seen map[string]struct{}
|
||||
}
|
||||
|
||||
// note records a decoding problem (malformed field, unknown value, swallowed
|
||||
// sub-parse error), deduping identical messages. ConvertV1ToV2 surfaces these
|
||||
// via errIfHasMalformedFields.
|
||||
func (d *v1Decoder) note(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
if _, dup := d.seen[msg]; dup {
|
||||
return
|
||||
}
|
||||
if d.seen == nil {
|
||||
d.seen = make(map[string]struct{})
|
||||
}
|
||||
d.seen[msg] = struct{}{}
|
||||
d.bad = append(d.bad, msg)
|
||||
}
|
||||
|
||||
// noteMalformedField records a v1 field present with the wrong Go type.
|
||||
func (d *v1Decoder) noteMalformedField(field string, raw any) {
|
||||
d.note("%q has unexpected type %T", field, raw)
|
||||
}
|
||||
|
||||
// detailErr renders an error for a diagnostic note, unfolding the structured
|
||||
// detail our JSON binding attaches via WithAdditional. A plain %v on these
|
||||
// errors prints only the innermost message ("request body contains invalid
|
||||
// field value") and drops the field/type context that says which field was
|
||||
// wrong — the part that actually tells you what to fix.
|
||||
func detailErr(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
j := errors.AsJSON(err)
|
||||
if len(j.Errors) == 0 {
|
||||
return err.Error()
|
||||
}
|
||||
details := make([]string, 0, len(j.Errors))
|
||||
for _, e := range j.Errors {
|
||||
details = append(details, e.Message)
|
||||
}
|
||||
return j.Message + ": " + strings.Join(details, "; ")
|
||||
}
|
||||
|
||||
func (d *v1Decoder) errIfHasMalformedFields() error {
|
||||
if len(d.bad) == 0 {
|
||||
return nil
|
||||
}
|
||||
// One field per line: these lists run long (a bad widget query is reported
|
||||
// once per widget), and a single "; "-joined line is an unscannable wall.
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidData, "malformed v1 dashboard fields:\n %s", strings.Join(d.bad, "\n "))
|
||||
}
|
||||
|
||||
func readField[T any](d *v1Decoder, m map[string]any, key string) T {
|
||||
var zero T
|
||||
v, present := m[key]
|
||||
if !present || v == nil {
|
||||
return zero
|
||||
}
|
||||
t, ok := v.(T)
|
||||
if !ok {
|
||||
d.noteMalformedField(key, v)
|
||||
return zero
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (d *v1Decoder) readString(m map[string]any, key string) string {
|
||||
return readField[string](d, m, key)
|
||||
}
|
||||
func (d *v1Decoder) readFloat(m map[string]any, key string) float64 {
|
||||
return readField[float64](d, m, key)
|
||||
}
|
||||
func (d *v1Decoder) readBool(m map[string]any, key string) bool { return readField[bool](d, m, key) }
|
||||
func (d *v1Decoder) readArray(m map[string]any, key string) []any { return readField[[]any](d, m, key) }
|
||||
func (d *v1Decoder) readObject(m map[string]any, key string) map[string]any {
|
||||
return readField[map[string]any](d, m, key)
|
||||
}
|
||||
|
||||
// readInt narrows a numeric field to int (JSON numbers decode as float64).
|
||||
func (d *v1Decoder) readInt(m map[string]any, key string) int { return int(d.readFloat(m, key)) }
|
||||
|
||||
func (d *v1Decoder) readFloatPtr(m map[string]any, key string) *float64 {
|
||||
v, present := m[key]
|
||||
if !present || v == nil {
|
||||
return nil
|
||||
}
|
||||
f, ok := v.(float64)
|
||||
if !ok {
|
||||
d.noteMalformedField(key, v)
|
||||
return nil
|
||||
}
|
||||
return &f
|
||||
}
|
||||
|
||||
func (d *v1Decoder) readStringMap(m map[string]any, key string) map[string]string {
|
||||
raw := d.readObject(m, key)
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(raw))
|
||||
for k, v := range raw {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
d.noteMalformedField(key+"."+k, v)
|
||||
continue
|
||||
}
|
||||
out[k] = s
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (d *v1Decoder) readObjects(m map[string]any, key string) []map[string]any {
|
||||
raw := d.readArray(m, key)
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]map[string]any, 0, len(raw))
|
||||
for i, item := range raw {
|
||||
obj, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
d.noteMalformedField(fmt.Sprintf("%s[%d]", key, i), item)
|
||||
continue
|
||||
}
|
||||
out = append(out, obj)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// decodeMapInto converts an untyped map[string]any into a typed T by
|
||||
// round-tripping through JSON, letting encoding/json (struct tags, custom
|
||||
// UnmarshalJSON) do the field mapping instead of hand-copying out of the map.
|
||||
func decodeMapInto[T any](src map[string]any) (T, error) {
|
||||
var dst T
|
||||
bytes, err := json.Marshal(src)
|
||||
if err != nil {
|
||||
return dst, err
|
||||
}
|
||||
if err := json.Unmarshal(bytes, &dst); err != nil {
|
||||
return dst, err
|
||||
}
|
||||
return dst, nil
|
||||
}
|
||||
230
pkg/types/dashboardtypes/perses_v1_to_v2_layouts.go
Normal file
230
pkg/types/dashboardtypes/perses_v1_to_v2_layouts.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/perses/spec/go/common"
|
||||
"github.com/perses/spec/go/dashboard"
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Layouts (data.layout + data.panelMap)
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// convertV1Layouts groups v1 react-grid-layout entries into v2 grid layouts.
|
||||
// Membership is positional (as the frontend renders): each row widget owns the
|
||||
// panels below it until the next row; panels above the first row form an unnamed
|
||||
// grid with no section header. Collapsed rows are the exception — their children
|
||||
// live in panelMap[rowID].widgets, not `layout`.
|
||||
func (d *v1Decoder) convertV1Layouts(data StorableDashboardData, panels map[string]*Panel) []Layout {
|
||||
layout := d.readObjects(data, "layout")
|
||||
if len(layout) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// react-grid-layout can persist the same widget id more than once. Drop the
|
||||
// duplicates, mirroring the frontend's getUpdatedLayout: keep the first
|
||||
// occurrence in stored order and discard the rest (the losing entry's
|
||||
// geometry is thrown away, not merged). Dedupe here, before sortByPosition,
|
||||
// so "first" means first-in-stored-order as the frontend sees it — not
|
||||
// topmost. Entries with no id are left for the main loop to drop.
|
||||
seenWidgetIds := make(map[string]bool, len(layout))
|
||||
dedupedLayouts := layout[:0]
|
||||
for _, item := range layout {
|
||||
if id := d.readString(item, "i"); id != "" {
|
||||
if seenWidgetIds[id] {
|
||||
continue
|
||||
}
|
||||
seenWidgetIds[id] = true
|
||||
}
|
||||
dedupedLayouts = append(dedupedLayouts, item)
|
||||
}
|
||||
layout = dedupedLayouts
|
||||
|
||||
rows := d.extractRowsAndCollapsedWidgets(data)
|
||||
|
||||
// Skip collapsed-row children a malformed dashboard lists in `layout` too.
|
||||
isWidgetCollapsed := make(map[string]bool)
|
||||
for _, row := range rows {
|
||||
for _, child := range row.collapsedWidgets {
|
||||
if id := d.readString(child, "i"); id != "" {
|
||||
isWidgetCollapsed[id] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d.sortByPosition(layout)
|
||||
|
||||
type section struct {
|
||||
row *rowInfo // nil for the unnamed grid of ungrouped panels
|
||||
items []map[string]any
|
||||
}
|
||||
topSectionWithoutHeader := §ion{}
|
||||
sectionsWithHeader := make([]*section, 0, len(rows))
|
||||
currentRowHeader := topSectionWithoutHeader
|
||||
for _, item := range layout {
|
||||
id := d.readString(item, "i")
|
||||
if id == "" || isWidgetCollapsed[id] {
|
||||
// widgets in collapsed sectinos will be added when those sections' row widgets are handled.
|
||||
continue
|
||||
}
|
||||
if row, ok := rows[id]; ok {
|
||||
newRowHeader := §ion{row: row, items: d.extractValidLayoutItemsForCollapsedSection(row.collapsedWidgets, panels)}
|
||||
sectionsWithHeader = append(sectionsWithHeader, newRowHeader)
|
||||
// A collapsed row owns only its stashed children; later panels → ungrouped.
|
||||
if row.collapsed {
|
||||
currentRowHeader = topSectionWithoutHeader
|
||||
} else {
|
||||
currentRowHeader = newRowHeader
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Keep a layout entry only if its widget became a panel; otherwise (skipped
|
||||
// widget, deleted id, or the "__dropping-elem__" drag placeholder) it would
|
||||
// reference a panel that does not exist. Rows are handled above.
|
||||
if _, ok := panels[id]; !ok {
|
||||
continue
|
||||
}
|
||||
currentRowHeader.items = append(currentRowHeader.items, item)
|
||||
}
|
||||
|
||||
out := make([]Layout, 0, len(sectionsWithHeader)+1)
|
||||
if len(topSectionWithoutHeader.items) > 0 {
|
||||
out = append(out, d.buildV2GridLayout(nil, topSectionWithoutHeader.items))
|
||||
}
|
||||
for _, sec := range sectionsWithHeader {
|
||||
out = append(out, d.buildV2GridLayout(sec.row, sec.items))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// extractValidLayoutItemsForCollapsedSection keeps only the collapsed-row children
|
||||
// backed by a real panel, dropping ghosts. These come from panelMap and skip the
|
||||
// main loop's per-item panel check, so a grid never references a missing panel.
|
||||
func (d *v1Decoder) extractValidLayoutItemsForCollapsedSection(items []map[string]any, panels map[string]*Panel) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
if id := d.readString(item, "i"); id != "" {
|
||||
if _, ok := panels[id]; ok {
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type rowInfo struct {
|
||||
title string
|
||||
collapsed bool
|
||||
collapsedWidgets []map[string]any
|
||||
}
|
||||
|
||||
// extractRowsAndCollapsedWidgets returns the row widgets keyed by id; collapsed
|
||||
// rows also carry their children stashed under panelMap[id].widgets.
|
||||
func (d *v1Decoder) extractRowsAndCollapsedWidgets(data StorableDashboardData) map[string]*rowInfo {
|
||||
panelMap := d.readObject(data, "panelMap")
|
||||
rows := make(map[string]*rowInfo)
|
||||
for _, w := range d.readObjects(data, "widgets") {
|
||||
id := d.readString(w, "id")
|
||||
if d.readString(w, "panelTypes") != "row" || id == "" {
|
||||
continue
|
||||
}
|
||||
row := &rowInfo{title: d.readString(w, "title")}
|
||||
// Some templates store panelMap[id] as a bare []widgetID instead of the
|
||||
// canonical {widgets, collapsed}. The frontend treats such a non-object
|
||||
// entry as "not collapsed" (see GridCardLayout), so read it leniently: a
|
||||
// non-map yields nil, which reads as not collapsed.
|
||||
pm, _ := panelMap[id].(map[string]any)
|
||||
if d.readBool(pm, "collapsed") {
|
||||
row.collapsed = true
|
||||
row.collapsedWidgets = d.readObjects(pm, "widgets")
|
||||
}
|
||||
rows[id] = row
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// buildV2GridLayout builds one v2 grid. row is nil for the unnamed grid (no
|
||||
// display); otherwise the grid takes the row's title and collapse state. Items are
|
||||
// sorted by (y, x) then vertically compacted (see compactGridItemsVertically).
|
||||
func (d *v1Decoder) buildV2GridLayout(row *rowInfo, items []map[string]any) Layout {
|
||||
d.sortByPosition(items)
|
||||
|
||||
spec := dashboard.GridLayoutSpec{Items: make([]dashboard.GridItem, 0, len(items))}
|
||||
if row != nil {
|
||||
spec.Display = &dashboard.GridLayoutDisplay{
|
||||
Title: row.title,
|
||||
Collapse: &dashboard.GridLayoutCollapse{Open: !row.collapsed},
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
spec.Items = append(spec.Items, dashboard.GridItem{
|
||||
X: d.readInt(item, "x"),
|
||||
Y: d.readInt(item, "y"),
|
||||
Width: d.readInt(item, "w"),
|
||||
Height: d.readInt(item, "h"),
|
||||
Content: &common.JSONRef{Ref: fmt.Sprintf("#/spec/panels/%s", d.readString(item, "i"))},
|
||||
})
|
||||
}
|
||||
compactGridItemsVertically(spec.Items)
|
||||
return Layout{Kind: dashboard.KindGridLayout, Spec: &spec}
|
||||
}
|
||||
|
||||
// compactGridItemsVertically mirrors react-grid-layout's correctBounds+compact
|
||||
// (compactType "vertical", allowOverlap false): clamp each item into the grid (x,y>=0;
|
||||
// x+width<=cols by shifting left), then move sorted-first items up to fill space and
|
||||
// down past collisions. Fixes overlaps, gaps, and out-of-bounds coords so the migrated
|
||||
// grid matches the v1 UI and passes v2 validation.
|
||||
func compactGridItemsVertically(items []dashboard.GridItem) {
|
||||
collides := func(a, b dashboard.GridItem) bool {
|
||||
return a.X < b.X+b.Width && b.X < a.X+a.Width && a.Y < b.Y+b.Height && b.Y < a.Y+a.Height
|
||||
}
|
||||
firstCollision := func(l dashboard.GridItem, placed []dashboard.GridItem) (dashboard.GridItem, bool) {
|
||||
for _, p := range placed {
|
||||
if collides(l, p) {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return dashboard.GridItem{}, false
|
||||
}
|
||||
for i := range items {
|
||||
l := items[i]
|
||||
if l.X+l.Width > gridColumnCount { // overflows right → shift left to fit
|
||||
l.X = gridColumnCount - l.Width
|
||||
}
|
||||
if l.X < 0 {
|
||||
l.X = 0
|
||||
}
|
||||
if l.Y < 0 {
|
||||
l.Y = 0
|
||||
}
|
||||
for l.Y > 0 { // move up to fill space above
|
||||
up := l
|
||||
up.Y--
|
||||
if _, hit := firstCollision(up, items[:i]); hit {
|
||||
break
|
||||
}
|
||||
l.Y--
|
||||
}
|
||||
for { // then down past any collision with an already-placed item
|
||||
c, hit := firstCollision(l, items[:i])
|
||||
if !hit {
|
||||
break
|
||||
}
|
||||
l.Y = c.Y + c.Height
|
||||
}
|
||||
items[i] = l
|
||||
}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) sortByPosition(items []map[string]any) {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if yi, yj := d.readInt(items[i], "y"), d.readInt(items[j], "y"); yi != yj {
|
||||
return yi < yj
|
||||
}
|
||||
return d.readInt(items[i], "x") < d.readInt(items[j], "x")
|
||||
})
|
||||
}
|
||||
473
pkg/types/dashboardtypes/perses_v1_to_v2_panels.go
Normal file
473
pkg/types/dashboardtypes/perses_v1_to_v2_panels.go
Normal file
@@ -0,0 +1,473 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Widgets → Panels
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// convertV1Panels walks the v1 `widgets` array and produces v2 panels keyed by
|
||||
// the v1 widget id. WidgetRow entries (panelTypes == "row") are dropped here
|
||||
// and consumed by convertV1Layouts as section headers.
|
||||
func (d *v1Decoder) convertV1Panels(raw any) map[string]*Panel {
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
widgetsRaw, ok := raw.([]any)
|
||||
if !ok {
|
||||
d.noteMalformedField("widgets", raw)
|
||||
return nil
|
||||
}
|
||||
panels := make(map[string]*Panel, len(widgetsRaw))
|
||||
for i, widgetRaw := range widgetsRaw {
|
||||
widget, ok := widgetRaw.(map[string]any)
|
||||
if !ok {
|
||||
d.noteMalformedField(fmt.Sprintf("widgets[%d]", i), widgetRaw)
|
||||
continue
|
||||
}
|
||||
id := d.readString(widget, "id")
|
||||
if id == "" {
|
||||
// dead widget that can't be referenced anywhere, skipping.
|
||||
continue
|
||||
}
|
||||
var panel *Panel
|
||||
panelType := d.readString(widget, "panelTypes")
|
||||
switch panelType {
|
||||
case "graph":
|
||||
panel = d.convertGraphWidget(widget)
|
||||
case "bar":
|
||||
panel = d.convertBarWidget(widget)
|
||||
case "value":
|
||||
panel = d.convertValueWidget(widget)
|
||||
case "pie":
|
||||
panel = d.convertPieWidget(widget)
|
||||
case "table":
|
||||
panel = d.convertTableWidget(widget)
|
||||
case "histogram":
|
||||
panel = d.convertHistogramWidget(widget)
|
||||
case "list":
|
||||
panel = d.convertListWidget(widget)
|
||||
case "row":
|
||||
// "row" (section header) is handled by the layout pass;
|
||||
continue
|
||||
default:
|
||||
d.note("widgets[%d] has unknown panel type %q", i, panelType)
|
||||
}
|
||||
if panel == nil {
|
||||
continue
|
||||
}
|
||||
panels[id] = panel
|
||||
}
|
||||
return panels
|
||||
}
|
||||
|
||||
func (d *v1Decoder) convertGraphWidget(w map[string]any) *Panel {
|
||||
return &Panel{
|
||||
Kind: "Panel",
|
||||
Spec: PanelSpec{
|
||||
Display: d.widgetDisplay(w),
|
||||
Plugin: PanelPlugin{
|
||||
Kind: PanelKindTimeSeries,
|
||||
Spec: &TimeSeriesPanelSpec{
|
||||
Visualization: TimeSeriesVisualization{
|
||||
BasicVisualization: d.basicVisualization(w),
|
||||
FillSpans: d.readBool(w, "fillSpans"),
|
||||
},
|
||||
Formatting: d.panelFormatting(w),
|
||||
ChartAppearance: TimeSeriesChartAppearance{
|
||||
LineInterpolation: mapV1Enum(d.readString(w, "lineInterpolation"), LineInterpolationSpline,
|
||||
LineInterpolationLinear, LineInterpolationSpline, LineInterpolationStepAfter, LineInterpolationStepBefore),
|
||||
ShowPoints: d.readBool(w, "showPoints"),
|
||||
LineStyle: mapV1Enum(d.readString(w, "lineStyle"), LineStyleSolid, LineStyleSolid, LineStyleDashed),
|
||||
FillMode: mapV1Enum(d.readString(w, "fillMode"), FillModeNone, FillModeSolid, FillModeGradient, FillModeNone),
|
||||
SpanGaps: mapV1SpanGaps(w["spanGaps"]),
|
||||
},
|
||||
Axes: d.axesFromWidget(w),
|
||||
Legend: d.legendFromWidget(w),
|
||||
Thresholds: d.mapV1ThresholdsWithLabel(w),
|
||||
},
|
||||
},
|
||||
Queries: d.convertV1WidgetQuery(w, PanelKindTimeSeries),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) convertBarWidget(w map[string]any) *Panel {
|
||||
return &Panel{
|
||||
Kind: "Panel",
|
||||
Spec: PanelSpec{
|
||||
Display: d.widgetDisplay(w),
|
||||
Plugin: PanelPlugin{
|
||||
Kind: PanelKindBarChart,
|
||||
Spec: &BarChartPanelSpec{
|
||||
Visualization: BarChartVisualization{
|
||||
BasicVisualization: d.basicVisualization(w),
|
||||
FillSpans: d.readBool(w, "fillSpans"),
|
||||
StackedBarChart: d.readBool(w, "stackedBarChart"),
|
||||
},
|
||||
Formatting: d.panelFormatting(w),
|
||||
Axes: d.axesFromWidget(w),
|
||||
Legend: d.legendFromWidget(w),
|
||||
Thresholds: d.mapV1ThresholdsWithLabel(w),
|
||||
},
|
||||
},
|
||||
Queries: d.convertV1WidgetQuery(w, PanelKindBarChart),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) convertValueWidget(w map[string]any) *Panel {
|
||||
return &Panel{
|
||||
Kind: "Panel",
|
||||
Spec: PanelSpec{
|
||||
Display: d.widgetDisplay(w),
|
||||
Plugin: PanelPlugin{
|
||||
Kind: PanelKindNumber,
|
||||
Spec: &NumberPanelSpec{
|
||||
Visualization: d.basicVisualization(w),
|
||||
Formatting: d.panelFormatting(w),
|
||||
Thresholds: d.mapV1ComparisonThresholds(w),
|
||||
},
|
||||
},
|
||||
Queries: d.convertV1WidgetQuery(w, PanelKindNumber),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) convertPieWidget(w map[string]any) *Panel {
|
||||
return &Panel{
|
||||
Kind: "Panel",
|
||||
Spec: PanelSpec{
|
||||
Display: d.widgetDisplay(w),
|
||||
Plugin: PanelPlugin{
|
||||
Kind: PanelKindPieChart,
|
||||
Spec: &PieChartPanelSpec{
|
||||
Visualization: d.basicVisualization(w),
|
||||
Formatting: d.panelFormatting(w),
|
||||
Legend: d.legendFromWidget(w),
|
||||
},
|
||||
},
|
||||
Queries: d.convertV1WidgetQuery(w, PanelKindPieChart),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) convertTableWidget(w map[string]any) *Panel {
|
||||
return &Panel{
|
||||
Kind: "Panel",
|
||||
Spec: PanelSpec{
|
||||
Display: d.widgetDisplay(w),
|
||||
Plugin: PanelPlugin{
|
||||
Kind: PanelKindTable,
|
||||
Spec: &TablePanelSpec{
|
||||
Visualization: d.basicVisualization(w),
|
||||
Formatting: TableFormatting{
|
||||
ColumnUnits: d.readStringMap(w, "columnUnits"),
|
||||
DecimalPrecision: mapV1Precision(w["decimalPrecision"]),
|
||||
},
|
||||
Thresholds: d.mapV1TableThresholds(w),
|
||||
},
|
||||
},
|
||||
Queries: d.convertV1WidgetQuery(w, PanelKindTable),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) convertHistogramWidget(w map[string]any) *Panel {
|
||||
return &Panel{
|
||||
Kind: "Panel",
|
||||
Spec: PanelSpec{
|
||||
Display: d.widgetDisplay(w),
|
||||
Plugin: PanelPlugin{
|
||||
Kind: PanelKindHistogram,
|
||||
Spec: &HistogramPanelSpec{
|
||||
HistogramBuckets: HistogramBuckets{
|
||||
BucketCount: d.readFloatPtr(w, "bucketCount"),
|
||||
BucketWidth: d.readFloatPtr(w, "bucketWidth"),
|
||||
MergeAllActiveQueries: d.readBool(w, "mergeAllActiveQueries"),
|
||||
},
|
||||
Legend: d.legendFromWidget(w),
|
||||
},
|
||||
},
|
||||
Queries: d.convertV1WidgetQuery(w, PanelKindHistogram),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) convertListWidget(w map[string]any) *Panel {
|
||||
return &Panel{
|
||||
Kind: "Panel",
|
||||
Spec: PanelSpec{
|
||||
Display: d.widgetDisplay(w),
|
||||
Plugin: PanelPlugin{
|
||||
Kind: PanelKindList,
|
||||
Spec: &ListPanelSpec{
|
||||
SelectFields: d.mapV1SelectFields(w),
|
||||
},
|
||||
},
|
||||
Queries: d.convertV1WidgetQuery(w, PanelKindList),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Panel-spec shared helpers
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
func (d *v1Decoder) widgetDisplay(w map[string]any) Display {
|
||||
return Display{Name: d.readString(w, "title"), Description: d.readString(w, "description")}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) basicVisualization(w map[string]any) BasicVisualization {
|
||||
return BasicVisualization{TimePreference: mapV1TimePreference(d.readString(w, "timePreferance"))}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) panelFormatting(w map[string]any) PanelFormatting {
|
||||
return PanelFormatting{Unit: d.readString(w, "yAxisUnit"), DecimalPrecision: mapV1Precision(w["decimalPrecision"])}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) axesFromWidget(w map[string]any) Axes {
|
||||
return Axes{
|
||||
SoftMin: d.readFloatPtr(w, "softMin"),
|
||||
SoftMax: d.readFloatPtr(w, "softMax"),
|
||||
IsLogScale: d.readBool(w, "isLogScale"),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) legendFromWidget(w map[string]any) Legend {
|
||||
return Legend{
|
||||
Position: mapV1Enum(d.readString(w, "legendPosition"), LegendPositionBottom, LegendPositionBottom, LegendPositionRight),
|
||||
CustomColors: d.readStringMap(w, "customLegendColors"),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) mapV1SelectFields(w map[string]any) []telemetrytypes.TelemetryFieldKey {
|
||||
field := "selectedLogFields"
|
||||
raw := d.readArray(w, field)
|
||||
if len(raw) == 0 {
|
||||
field = "selectedTracesFields"
|
||||
raw = d.readArray(w, field)
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
normalizePreV5FieldKeys(raw)
|
||||
fields, err := decodeTelemetryFields(raw)
|
||||
if err != nil {
|
||||
d.note("widget %q has malformed %s: %v", d.readString(w, "id"), field, err)
|
||||
return nil
|
||||
}
|
||||
// Drop nameless entries (blank column rows) — v2 requires a name, and the v1
|
||||
// UI renders nothing for them anyway.
|
||||
out := fields[:0]
|
||||
for _, f := range fields {
|
||||
if f.Name != "" {
|
||||
out = append(out, f)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func decodeTelemetryFields(raw []any) ([]telemetrytypes.TelemetryFieldKey, error) {
|
||||
bytes, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var fields []telemetrytypes.TelemetryFieldKey
|
||||
if err := json.Unmarshal(bytes, &fields); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Panel field mappers
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// v1 stores timePreferance as `GLOBAL_TIME`, `LAST_5_MIN`, … (see
|
||||
// frontend/src/container/NewWidget/RightContainer/timeItems.ts). v2 uses the
|
||||
// lowercase form, so the translation is just downcase.
|
||||
func mapV1TimePreference(s string) TimePreference {
|
||||
if s == "" {
|
||||
return TimePreferenceGlobalTime
|
||||
}
|
||||
candidate := TimePreference{valuer.NewString(strings.ToLower(s))}
|
||||
for _, allowed := range candidate.Enum() {
|
||||
if allowed == candidate {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return TimePreferenceGlobalTime
|
||||
}
|
||||
|
||||
// mapV1Precision is polymorphic (string|number), so it type-switches the raw
|
||||
// value rather than reading through a typed accessor.
|
||||
func mapV1Precision(raw any) PrecisionOption {
|
||||
switch v := raw.(type) {
|
||||
case string:
|
||||
candidate := PrecisionOption{valuer.NewString(v)}
|
||||
for _, allowed := range candidate.Enum() {
|
||||
if allowed == candidate {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
case float64:
|
||||
n := int(v)
|
||||
if n >= 0 && n <= 4 {
|
||||
return PrecisionOption{valuer.NewString(strconv.Itoa(n))}
|
||||
}
|
||||
}
|
||||
return PrecisionOption2
|
||||
}
|
||||
|
||||
// mapV1Enum picks the v1 string value if it matches one of the allowed v2
|
||||
// values, otherwise returns the fallback. v1 frontend enums (lineInterpolation,
|
||||
// lineStyle, fillMode, legendPosition) already use the v2 lowercase form.
|
||||
func mapV1Enum[T interface{ StringValue() string }](s string, fallback T, allowed ...T) T {
|
||||
if s == "" {
|
||||
return fallback
|
||||
}
|
||||
for _, a := range allowed {
|
||||
if a.StringValue() == s {
|
||||
return a
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// v1 spanGaps is `boolean | number`. true → span every gap; false → never span;
|
||||
// a number is interpreted (per frontend SeriesProps.spanGaps docs) as an
|
||||
// X-axis threshold in seconds. Polymorphic, so it type-switches the raw value.
|
||||
func mapV1SpanGaps(raw any) SpanGaps {
|
||||
switch v := raw.(type) {
|
||||
case bool:
|
||||
if v {
|
||||
return SpanGaps{FillOnlyBelow: false}
|
||||
}
|
||||
return SpanGaps{FillOnlyBelow: true}
|
||||
case float64:
|
||||
dur, err := valuer.ParseTextDuration(time.Duration(v * float64(time.Second)).String())
|
||||
if err != nil {
|
||||
return SpanGaps{FillOnlyBelow: false}
|
||||
}
|
||||
return SpanGaps{FillOnlyBelow: true, FillLessThan: dur}
|
||||
}
|
||||
return SpanGaps{FillOnlyBelow: false}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) mapV1ThresholdsWithLabel(w map[string]any) []ThresholdWithLabel {
|
||||
rawSlice := d.readObjects(w, "thresholds")
|
||||
if len(rawSlice) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]ThresholdWithLabel, 0, len(rawSlice))
|
||||
for _, t := range rawSlice {
|
||||
color := d.readString(t, "thresholdColor")
|
||||
label := d.readString(t, "thresholdLabel")
|
||||
if color == "" || label == "" {
|
||||
// v2 ThresholdWithLabel requires both; drop entries that wouldn't validate.
|
||||
continue
|
||||
}
|
||||
value := d.readFloat(t, "thresholdValue")
|
||||
out = append(out, ThresholdWithLabel{Value: &value, Unit: d.readString(t, "thresholdUnit"), Color: color, Label: label})
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (d *v1Decoder) mapV1ComparisonThresholds(w map[string]any) []ComparisonThreshold {
|
||||
rawSlice := d.readObjects(w, "thresholds")
|
||||
if len(rawSlice) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]ComparisonThreshold, 0, len(rawSlice))
|
||||
for _, t := range rawSlice {
|
||||
color := d.readString(t, "thresholdColor")
|
||||
if color == "" {
|
||||
continue
|
||||
}
|
||||
value := d.readFloat(t, "thresholdValue")
|
||||
out = append(out, ComparisonThreshold{
|
||||
Value: &value,
|
||||
Operator: d.mapV1ComparisonOperator(d.readString(t, "thresholdOperator")),
|
||||
Unit: d.readString(t, "thresholdUnit"),
|
||||
Color: color,
|
||||
Format: mapV1ThresholdFormat(d.readString(t, "thresholdFormat")),
|
||||
})
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (d *v1Decoder) mapV1TableThresholds(w map[string]any) []TableThreshold {
|
||||
rawSlice := d.readObjects(w, "thresholds")
|
||||
if len(rawSlice) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]TableThreshold, 0, len(rawSlice))
|
||||
for _, t := range rawSlice {
|
||||
color := d.readString(t, "thresholdColor")
|
||||
columnName := d.readString(t, "thresholdTableOptions")
|
||||
if color == "" || columnName == "" {
|
||||
continue
|
||||
}
|
||||
value := d.readFloat(t, "thresholdValue")
|
||||
out = append(out, TableThreshold{
|
||||
ComparisonThreshold: ComparisonThreshold{
|
||||
Value: &value,
|
||||
Operator: d.mapV1ComparisonOperator(d.readString(t, "thresholdOperator")),
|
||||
Unit: d.readString(t, "thresholdUnit"),
|
||||
Color: color,
|
||||
Format: mapV1ThresholdFormat(d.readString(t, "thresholdFormat")),
|
||||
},
|
||||
ColumnName: columnName,
|
||||
})
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (d *v1Decoder) mapV1ComparisonOperator(s string) ComparisonOperator {
|
||||
switch s {
|
||||
case ">":
|
||||
return ComparisonOperatorAbove
|
||||
case ">=":
|
||||
return ComparisonOperatorAboveOrEqual
|
||||
case "<":
|
||||
return ComparisonOperatorBelow
|
||||
case "<=":
|
||||
return ComparisonOperatorBelowOrEqual
|
||||
case "=":
|
||||
return ComparisonOperatorEqual
|
||||
case "!=":
|
||||
return ComparisonOperatorNotEqual
|
||||
default:
|
||||
d.note("threshold has unknown comparison operator %q", s)
|
||||
return ComparisonOperatorAbove
|
||||
}
|
||||
}
|
||||
|
||||
func mapV1ThresholdFormat(s string) ThresholdFormat {
|
||||
switch strings.ToLower(s) {
|
||||
case "background":
|
||||
return ThresholdFormatBackground
|
||||
case "text":
|
||||
return ThresholdFormatText
|
||||
}
|
||||
return ThresholdFormatText
|
||||
}
|
||||
258
pkg/types/dashboardtypes/perses_v1_to_v2_queries.go
Normal file
258
pkg/types/dashboardtypes/perses_v1_to_v2_queries.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Queries
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// convertV1WidgetQuery returns exactly one Query (per Spec.Validate). The kind
|
||||
// chosen depends on the v1 widget query shape:
|
||||
// - a single query (promql / clickhouse_sql / builder) → its native kind
|
||||
// - multiple queries → signoz/CompositeQuery
|
||||
//
|
||||
// A single query is never wrapped in a CompositeQuery; in particular List
|
||||
// panels accept only a bare signoz/BuilderQuery. Builder queries are routed
|
||||
// through qb.WrapInV5Envelope (in collectV1QueryEnvelopes), which translates v4
|
||||
// builder-field names (orderBy/selectColumns/dataSource) into their v5
|
||||
// equivalents and adds the `signal` field required by BuilderQuerySpec's
|
||||
// per-signal dispatch.
|
||||
func (d *v1Decoder) convertV1WidgetQuery(widget map[string]any, panelKind PanelPluginKind) []Query {
|
||||
envelopes, signal := d.collectV1QueryEnvelopes(widget, panelKind)
|
||||
if len(envelopes) == 0 {
|
||||
return nil
|
||||
}
|
||||
requestType := requestTypeForPanel(panelKind)
|
||||
|
||||
// A single query keeps its native kind — never wrapped in a CompositeQuery.
|
||||
if len(envelopes) == 1 {
|
||||
if q := singleQueryFromEnvelope(envelopes[0], requestType, signal); q != nil {
|
||||
return []Query{*q}
|
||||
}
|
||||
}
|
||||
|
||||
// Default: wrap in CompositeQuery.
|
||||
composite, err := parseCompositeFromEnvelopes(envelopes)
|
||||
if err != nil || composite == nil {
|
||||
d.note("widget %q: could not build query from %d envelope(s): %s", d.readString(widget, "id"), len(envelopes), detailErr(err))
|
||||
return nil
|
||||
}
|
||||
return []Query{{
|
||||
Kind: requestType,
|
||||
Spec: QuerySpec{
|
||||
Plugin: QueryPlugin{Kind: QueryKindComposite, Spec: composite},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// requestTypeForPanel maps a v2 panel plugin kind to the request type (result
|
||||
// shape) its queries produce. Mirrors the frontend's panelTypeToRequestType
|
||||
// (buildQueryRangeRequest.ts): time series for line/bar/histogram (histogram
|
||||
// bins client-side from raw time series, V1 parity), scalar for
|
||||
// number/pie/table, raw rows for list.
|
||||
func requestTypeForPanel(panelKind PanelPluginKind) qb.RequestType {
|
||||
switch panelKind {
|
||||
case PanelKindTimeSeries, PanelKindBarChart, PanelKindHistogram:
|
||||
return qb.RequestTypeTimeSeries
|
||||
case PanelKindNumber, PanelKindPieChart, PanelKindTable:
|
||||
return qb.RequestTypeScalar
|
||||
case PanelKindList:
|
||||
return qb.RequestTypeRaw
|
||||
}
|
||||
return qb.RequestTypeTimeSeries
|
||||
}
|
||||
|
||||
// collectV1QueryEnvelopes inspects widget.query.queryType and produces a
|
||||
// flattened list of v5-shaped envelopes. The returned signal is the dominant
|
||||
// builder signal (if any), used for typed builder-query dispatch.
|
||||
func (d *v1Decoder) collectV1QueryEnvelopes(widget map[string]any, panelKind PanelPluginKind) ([]map[string]any, telemetrytypes.Signal) {
|
||||
queryMap := d.readObject(widget, "query")
|
||||
if queryMap == nil {
|
||||
return nil, telemetrytypes.Signal{}
|
||||
}
|
||||
rowLimitPanel := panelKind == PanelKindList || panelKind == PanelKindTable
|
||||
// Raw (list) panels legitimately have no aggregation; every other panel needs one.
|
||||
needsAggregation := requestTypeForPanel(panelKind) != qb.RequestTypeRaw
|
||||
|
||||
queryType := d.readString(queryMap, "queryType")
|
||||
switch queryType {
|
||||
case "promql":
|
||||
var out []map[string]any
|
||||
for _, q := range d.readObjects(queryMap, "promql") {
|
||||
out = append(out, promQLEnvelope(q))
|
||||
}
|
||||
return out, telemetrytypes.Signal{}
|
||||
|
||||
case "clickhouse_sql":
|
||||
var out []map[string]any
|
||||
for _, q := range d.readObjects(queryMap, "clickhouse_sql") {
|
||||
out = append(out, clickhouseEnvelope(q))
|
||||
}
|
||||
return out, telemetrytypes.Signal{}
|
||||
|
||||
case "builder":
|
||||
builder := d.readObject(queryMap, "builder")
|
||||
if builder == nil {
|
||||
return nil, telemetrytypes.Signal{}
|
||||
}
|
||||
var out []map[string]any
|
||||
var signal telemetrytypes.Signal
|
||||
widgetType := d.readString(widget, "panelTypes")
|
||||
for _, q := range d.readObjects(builder, "queryData") {
|
||||
normalizePreV5QueryData(q, widgetType)
|
||||
normalizePreV5SelectColumns(q)
|
||||
normalizePreV5PageSize(q, rowLimitPanel)
|
||||
if needsAggregation {
|
||||
ensureDefaultAggregation(q)
|
||||
}
|
||||
name := d.readString(q, "queryName")
|
||||
out = append(out, qb.WrapInV5Envelope(name, q, string(qb.QueryTypeBuilder.StringValue())))
|
||||
if signal.IsZero() {
|
||||
signal = signalFromDataSource(q["dataSource"])
|
||||
}
|
||||
}
|
||||
for _, f := range d.readObjects(builder, "queryFormulas") {
|
||||
normalizePreV5QueryData(f, widgetType)
|
||||
name := d.readString(f, "queryName")
|
||||
out = append(out, qb.WrapInV5Envelope(name, f, string(qb.QueryTypeFormula.StringValue())))
|
||||
}
|
||||
for _, op := range d.readObjects(builder, "queryTraceOperator") {
|
||||
normalizePreV5QueryData(op, widgetType)
|
||||
name := d.readString(op, "queryName")
|
||||
out = append(out, qb.WrapInV5Envelope(name, op, string(qb.QueryTypeTraceOperator.StringValue())))
|
||||
}
|
||||
return out, signal
|
||||
default:
|
||||
d.note("widget %q has unknown queryType %q", d.readString(widget, "id"), queryType)
|
||||
}
|
||||
return nil, telemetrytypes.Signal{}
|
||||
}
|
||||
|
||||
func promQLEnvelope(q map[string]any) map[string]any {
|
||||
return map[string]any{
|
||||
"type": qb.QueryTypePromQL.StringValue(),
|
||||
"spec": map[string]any{
|
||||
"name": q["name"],
|
||||
"query": q["query"],
|
||||
"disabled": q["disabled"],
|
||||
"legend": q["legend"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func clickhouseEnvelope(q map[string]any) map[string]any {
|
||||
return map[string]any{
|
||||
"type": qb.QueryTypeClickHouseSQL.StringValue(),
|
||||
"spec": map[string]any{
|
||||
"name": q["name"],
|
||||
"query": q["query"],
|
||||
"disabled": q["disabled"],
|
||||
"legend": q["legend"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// singleQueryFromEnvelope returns a typed Query for one envelope, using its
|
||||
// native query kind (promql/clickhouse_sql/builder) rather than wrapping it in
|
||||
// a CompositeQuery. A bare signoz/BuilderQuery is valid for every panel kind
|
||||
// and is the only kind List panels accept.
|
||||
func singleQueryFromEnvelope(envelope map[string]any, requestType qb.RequestType, signal telemetrytypes.Signal) *Query {
|
||||
t, _ := envelope["type"].(string)
|
||||
spec, _ := envelope["spec"].(map[string]any)
|
||||
switch t {
|
||||
case qb.QueryTypePromQL.StringValue():
|
||||
prom, err := decodeMapInto[qb.PromQuery](spec)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &Query{
|
||||
Kind: requestType,
|
||||
Spec: QuerySpec{
|
||||
Name: prom.Name,
|
||||
Plugin: QueryPlugin{Kind: QueryKindPromQL, Spec: &prom},
|
||||
},
|
||||
}
|
||||
case qb.QueryTypeClickHouseSQL.StringValue():
|
||||
ch, err := decodeMapInto[qb.ClickHouseQuery](spec)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &Query{
|
||||
Kind: requestType,
|
||||
Spec: QuerySpec{
|
||||
Name: ch.Name,
|
||||
Plugin: QueryPlugin{Kind: QueryKindClickHouseSQL, Spec: &ch},
|
||||
},
|
||||
}
|
||||
case qb.QueryTypeBuilder.StringValue():
|
||||
builderSpec := parseBuilderQuerySpec(spec, signal)
|
||||
if builderSpec == nil {
|
||||
return nil
|
||||
}
|
||||
name, _ := spec["name"].(string)
|
||||
return &Query{
|
||||
Kind: requestType,
|
||||
Spec: QuerySpec{
|
||||
Name: name,
|
||||
Plugin: QueryPlugin{Kind: QueryKindBuilder, Spec: &BuilderQuerySpec{Spec: builderSpec}},
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseCompositeFromEnvelopes(envelopes []map[string]any) (*CompositeQuerySpec, error) {
|
||||
bytes, err := json.Marshal(envelopes)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "marshal v1 query envelopes")
|
||||
}
|
||||
var parsed []qb.QueryEnvelope
|
||||
if err := json.Unmarshal(bytes, &parsed); err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidWidgetQuery, "decode v5 query envelopes")
|
||||
}
|
||||
return &CompositeQuerySpec{Queries: parsed}, nil
|
||||
}
|
||||
|
||||
func parseBuilderQuerySpec(rawSpec any, signal telemetrytypes.Signal) any {
|
||||
spec, ok := rawSpec.(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if !signal.IsZero() {
|
||||
spec["signal"] = signal.StringValue()
|
||||
}
|
||||
bytes, err := json.Marshal(spec)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
parsed, err := qb.UnmarshalBuilderQueryBySignal(bytes)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
// signalFromDataSource maps a v1 data-source string to a v5 signal. Casing
|
||||
// varies by source: builder queries store lowercase ("traces"), while variable
|
||||
// `dynamicVariablesSource` stores capitalized ("Traces"), so match
|
||||
// case-insensitively. Unknown values (e.g. "All telemetry") map to the zero
|
||||
// Signal.
|
||||
func signalFromDataSource(raw any) telemetrytypes.Signal {
|
||||
s, _ := raw.(string)
|
||||
switch strings.ToLower(s) {
|
||||
case "traces":
|
||||
return telemetrytypes.SignalTraces
|
||||
case "logs":
|
||||
return telemetrytypes.SignalLogs
|
||||
case "metrics":
|
||||
return telemetrytypes.SignalMetrics
|
||||
}
|
||||
return telemetrytypes.Signal{}
|
||||
}
|
||||
182
pkg/types/dashboardtypes/perses_v1_to_v2_queries_malformed.go
Normal file
182
pkg/types/dashboardtypes/perses_v1_to_v2_queries_malformed.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/transition"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Malformed-field normalization
|
||||
// ══════════════════════════════════════════════
|
||||
//
|
||||
// Pre-v5 query-body reshapes for dashboards whose bodies aren't actually v5-shaped
|
||||
// (e.g. stamped version:"v5" but never upgraded). The bulk of the upgrade is
|
||||
// delegated to transition.MigrateQueryDataShapeSafe (see normalizePreV5QueryData);
|
||||
// this file keeps only the reshapes it doesn't cover.
|
||||
|
||||
// preV5Migrator runs transition's shape-safe (idempotent) v4→v5 upgrade. Stateless
|
||||
// after construction, so a shared instance with a discard logger / no ambiguity
|
||||
// keys is fine.
|
||||
var preV5Migrator = transition.NewDashboardMigrateV5(slog.New(slog.DiscardHandler), nil, nil)
|
||||
|
||||
// normalizePreV5QueryData upgrades one builder queryData/formula in place: the
|
||||
// shared migrator, then a reshape of any existing aggregations[] it leaves alone.
|
||||
func normalizePreV5QueryData(query map[string]any, widgetType string) {
|
||||
preV5Migrator.MigrateQueryDataShapeSafe(context.Background(), query, widgetType)
|
||||
normalizePreV5LogTraceAggregations(query)
|
||||
}
|
||||
|
||||
// aggExprRe matches one "func(args)" with an optional "as alias". Mirrors the
|
||||
// frontend's parseAggregations regex; matching only well-formed func(args)
|
||||
// discards trailing junk ("sum(x) ) )" → "sum(x)").
|
||||
var aggExprRe = regexp.MustCompile(`([a-zA-Z0-9_]+\([^)]*\))(?:\s*as\s+('[^']*'|"[^"]*"|[a-zA-Z0-9_-]+))?`)
|
||||
|
||||
// normalizePreV5LogTraceAggregations reshapes an existing logs/traces aggregations[]
|
||||
// via parseAggregations (extract func(args), lift inline "as alias", split
|
||||
// multi-part, drop metric-only fields; empty → count()). Covers the case the
|
||||
// migrator skips: it builds from flat fields but leaves a present-but-malformed
|
||||
// aggregations[] alone. A query with none is left as-is.
|
||||
func normalizePreV5LogTraceAggregations(query map[string]any) {
|
||||
switch signalFromDataSource(query["dataSource"]) {
|
||||
case telemetrytypes.SignalLogs, telemetrytypes.SignalTraces:
|
||||
default:
|
||||
return
|
||||
}
|
||||
aggs, ok := query["aggregations"].([]any)
|
||||
if !ok || len(aggs) == 0 {
|
||||
return
|
||||
}
|
||||
out := make([]any, 0, len(aggs))
|
||||
for _, a := range aggs {
|
||||
agg, ok := a.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
expr, _ := agg["expression"].(string)
|
||||
alias, _ := agg["alias"].(string)
|
||||
parsed := parseAggregations(expr, alias)
|
||||
if len(parsed) == 0 {
|
||||
parsed = []any{map[string]any{"expression": "count()"}}
|
||||
}
|
||||
out = append(out, parsed...)
|
||||
}
|
||||
query["aggregations"] = out
|
||||
}
|
||||
|
||||
// ensureDefaultAggregation defaults an empty logs/traces aggregations[] to count(),
|
||||
// mirroring the frontend. Callers gate this to aggregation panels. Metrics are skipped:
|
||||
// count() can't stand in for a missing metricName.
|
||||
func ensureDefaultAggregation(query map[string]any) {
|
||||
switch signalFromDataSource(query["dataSource"]) {
|
||||
case telemetrytypes.SignalLogs, telemetrytypes.SignalTraces:
|
||||
default:
|
||||
return
|
||||
}
|
||||
if aggs, ok := query["aggregations"].([]any); ok && len(aggs) > 0 {
|
||||
return
|
||||
}
|
||||
query["aggregations"] = []any{map[string]any{"expression": "count()"}}
|
||||
}
|
||||
|
||||
// parseAggregations pulls every func(args) (with inline or passed-through alias,
|
||||
// quotes stripped) out of a v1 expression. Mirrors the frontend's
|
||||
// parseAggregations; empty result if none.
|
||||
func parseAggregations(expression, availableAlias string) []any {
|
||||
matches := aggExprRe.FindAllStringSubmatch(expression, -1)
|
||||
out := make([]any, 0, len(matches))
|
||||
for _, m := range matches {
|
||||
alias := m[2]
|
||||
if alias == "" {
|
||||
alias = availableAlias
|
||||
}
|
||||
agg := map[string]any{"expression": m[1]}
|
||||
if alias != "" {
|
||||
agg["alias"] = strings.Trim(alias, `'"`)
|
||||
}
|
||||
out = append(out, agg)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// normalizePreV5SelectColumns lets WrapInV5Envelope (which reads the old
|
||||
// {key,dataType,type}) handle selectColumns stored the v5 way ({name,…}): backfill
|
||||
// key/dataType/type from name/fieldDataType/fieldContext, drop columns with no
|
||||
// resolvable key. Inverse of normalizePreV5FieldKeys (the two consumers want
|
||||
// opposite shapes).
|
||||
func normalizePreV5SelectColumns(query map[string]any) {
|
||||
cols, ok := query["selectColumns"].([]any)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
out := make([]any, 0, len(cols))
|
||||
for _, c := range cols {
|
||||
col, ok := c.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := col["key"]; !ok {
|
||||
if name, ok := col["name"]; ok {
|
||||
col["key"] = name
|
||||
}
|
||||
}
|
||||
if _, ok := col["dataType"]; !ok {
|
||||
if fdt, ok := col["fieldDataType"]; ok {
|
||||
col["dataType"] = fdt
|
||||
}
|
||||
}
|
||||
if _, ok := col["type"]; !ok {
|
||||
if fc, ok := col["fieldContext"]; ok {
|
||||
col["type"] = fc
|
||||
}
|
||||
}
|
||||
if key, _ := col["key"].(string); key == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, col)
|
||||
}
|
||||
query["selectColumns"] = out
|
||||
}
|
||||
|
||||
// normalizePreV5FieldKeys renames list-panel field keys {key,dataType,type} →
|
||||
// {name,fieldDataType,fieldContext} in place (as WrapInV5Envelope does for
|
||||
// groupBy/orderBy). Entries already carrying "name" are left as-is.
|
||||
func normalizePreV5FieldKeys(fields []any) {
|
||||
for _, f := range fields {
|
||||
field, ok := f.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, hasName := field["name"]; hasName {
|
||||
continue
|
||||
}
|
||||
if key, ok := field["key"]; ok {
|
||||
field["name"] = key
|
||||
}
|
||||
if dataType, ok := field["dataType"]; ok {
|
||||
field["fieldDataType"] = dataType
|
||||
}
|
||||
if typ, ok := field["type"]; ok {
|
||||
field["fieldContext"] = typ
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// normalizePreV5PageSize backfills limit from the legacy pageSize (frontend's
|
||||
// `limit || pageSize`), for row-limited panels (list/table) only. Leaves a query
|
||||
// that already has limit, or a non-row-limited panel, untouched.
|
||||
func normalizePreV5PageSize(query map[string]any, rowLimitPanel bool) {
|
||||
if !rowLimitPanel {
|
||||
return
|
||||
}
|
||||
if limit, ok := query["limit"]; ok && limit != nil {
|
||||
return
|
||||
}
|
||||
if ps, ok := query["pageSize"]; ok {
|
||||
query["limit"] = ps
|
||||
}
|
||||
}
|
||||
122
pkg/types/dashboardtypes/perses_v1_to_v2_tags.go
Normal file
122
pkg/types/dashboardtypes/perses_v1_to_v2_tags.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Tags
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// v1 carries tags as a flat []string; v2 tags are (key, value) pairs. Each v1
|
||||
// string is normalized into a pair (separator split, empty-side fallback,
|
||||
// reserved-key prefix, `/` scrub). Tags that normalize to the same
|
||||
// (lower(key), lower(value)) within a dashboard are collapsed, first occurrence
|
||||
// winning the display casing.
|
||||
//
|
||||
// Characters still illegal after normalization (spaces, punctuation) are molded
|
||||
// to fit the tag validators: disallowed runs collapse to "_" (see moldTagField).
|
||||
|
||||
// defaultV1TagKey is the key assigned when a v1 tag string has no usable
|
||||
// separator (or one side of the split is empty).
|
||||
const defaultV1TagKey = "tag"
|
||||
|
||||
func (d *v1Decoder) convertV1TagsForOrg(orgID valuer.UUID, raw any) []*tagtypes.Tag {
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
rawTagsList, ok := raw.([]any)
|
||||
if !ok {
|
||||
d.noteMalformedField("tags", raw)
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(rawTagsList))
|
||||
tagsV2 := make([]*tagtypes.Tag, 0, len(rawTagsList))
|
||||
for i, rawTag := range rawTagsList {
|
||||
s, ok := rawTag.(string)
|
||||
if !ok {
|
||||
d.noteMalformedField(fmt.Sprintf("tags[%d]", i), rawTag)
|
||||
continue
|
||||
}
|
||||
key, value, ok := normalizeV1Tag(s)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
dedupKey := strings.ToLower(key) + "\x00" + strings.ToLower(value)
|
||||
if _, dup := seen[dedupKey]; dup {
|
||||
continue
|
||||
}
|
||||
seen[dedupKey] = struct{}{}
|
||||
tagsV2 = append(tagsV2, tagtypes.NewTag(orgID, coretypes.KindDashboard, key, value))
|
||||
}
|
||||
return tagsV2
|
||||
}
|
||||
|
||||
// normalizeV1Tag derives a (key, value) pair from one v1 tag string. After
|
||||
// splitting and molding both sides, a lone survivor becomes a value under the
|
||||
// default key; ok is false if neither survives.
|
||||
func normalizeV1Tag(s string) (string, string, bool) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
var rawKey, rawValue string
|
||||
switch {
|
||||
case strings.Contains(s, ":"):
|
||||
rawKey, rawValue, _ = strings.Cut(s, ":")
|
||||
// Only the first ":" separates key from value; collapse the rest.
|
||||
rawValue = strings.ReplaceAll(rawValue, ":", "_")
|
||||
case strings.Contains(s, "/"):
|
||||
rawKey, rawValue, _ = strings.Cut(s, "/")
|
||||
default:
|
||||
rawValue = s
|
||||
}
|
||||
rawKey = strings.TrimSpace(rawKey)
|
||||
rawValue = strings.TrimSpace(rawValue)
|
||||
|
||||
// Reserved-key collision: prefix "_" so the list-query DSL stays unambiguous.
|
||||
if _, reserved := reservedDSLKeys[DSLKey(strings.ToLower(rawKey))]; rawKey != "" && reserved {
|
||||
rawKey = "_" + rawKey
|
||||
}
|
||||
|
||||
key := moldTagField(rawKey, tagKeyDisallowed, tagKeyNotLead, tagtypes.MAX_LEN_TAG_KEY)
|
||||
value := moldTagField(rawValue, tagValueDisallowed, nil, tagtypes.MAX_LEN_TAG_VALUE)
|
||||
switch {
|
||||
case key == "" && value == "":
|
||||
return "", "", false
|
||||
case key == "":
|
||||
return defaultV1TagKey, value, true
|
||||
case value == "":
|
||||
return defaultV1TagKey, key, true
|
||||
default:
|
||||
return key, value, true
|
||||
}
|
||||
}
|
||||
|
||||
// Inverse of tagKeyRegex/tagValueRegex ("/" always rejected); tagKeyNotLead
|
||||
// matches a bad first char for a key. TestMoldedV1TagsPassValidation guards drift.
|
||||
var (
|
||||
tagKeyDisallowed = regexp.MustCompile(`[^a-zA-Z0-9$_@#{}:-]+`)
|
||||
tagValueDisallowed = regexp.MustCompile(`[^a-zA-Z0-9$_@#{}:.+=-]+`)
|
||||
tagKeyNotLead = regexp.MustCompile(`^[^a-zA-Z$_@{#]`)
|
||||
)
|
||||
|
||||
// moldTagField collapses disallowed runs to "_", prefixes "_" if notLead hits
|
||||
// the first char, and caps at max. Keeps a leading "_", trims a trailing one.
|
||||
func moldTagField(s string, disallowed, notLead *regexp.Regexp, max int) string {
|
||||
s = strings.TrimRight(disallowed.ReplaceAllString(s, "_"), "_")
|
||||
if s != "" && notLead != nil && notLead.MatchString(s) {
|
||||
s = "_" + s
|
||||
}
|
||||
if len(s) > max {
|
||||
s = strings.TrimRight(s[:max], "_")
|
||||
}
|
||||
return s
|
||||
}
|
||||
1499
pkg/types/dashboardtypes/perses_v1_to_v2_test.go
Normal file
1499
pkg/types/dashboardtypes/perses_v1_to_v2_test.go
Normal file
File diff suppressed because it is too large
Load Diff
173
pkg/types/dashboardtypes/perses_v1_to_v2_variables.go
Normal file
173
pkg/types/dashboardtypes/perses_v1_to_v2_variables.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/perses/spec/go/dashboard/variable"
|
||||
)
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Variables
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// convertV1Variables walks the v1 `variables` map (UUID-keyed) and produces an
|
||||
// ordered []Variable. Variables sort by `order` first, then by id for stable
|
||||
// output. v1 variable types map as follows:
|
||||
//
|
||||
// QUERY → ListVariable + signoz/QueryVariable
|
||||
// CUSTOM → ListVariable + signoz/CustomVariable
|
||||
// DYNAMIC → ListVariable + signoz/DynamicVariable
|
||||
// TEXTBOX → TextVariable
|
||||
func (d *v1Decoder) convertV1Variables(raw any) []Variable {
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
rawVariablesMap, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
d.noteMalformedField("variables", raw)
|
||||
return nil
|
||||
}
|
||||
type ordered struct {
|
||||
variableID string
|
||||
variableContent map[string]any
|
||||
order float64
|
||||
}
|
||||
entries := make([]ordered, 0, len(rawVariablesMap))
|
||||
for variableID, variableContentRaw := range rawVariablesMap {
|
||||
variableContent, ok := variableContentRaw.(map[string]any)
|
||||
if !ok {
|
||||
d.noteMalformedField("variables."+variableID, variableContentRaw)
|
||||
continue
|
||||
}
|
||||
entries = append(entries, ordered{variableID: variableID, variableContent: variableContent, order: d.readFloat(variableContent, "order")})
|
||||
}
|
||||
sort.SliceStable(entries, func(i, j int) bool {
|
||||
if entries[i].order != entries[j].order {
|
||||
return entries[i].order < entries[j].order
|
||||
}
|
||||
return entries[i].variableID < entries[j].variableID
|
||||
})
|
||||
|
||||
variablesV2 := make([]Variable, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
v, ok := d.convertV1Variable(e.variableContent)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
variablesV2 = append(variablesV2, v)
|
||||
}
|
||||
return variablesV2
|
||||
}
|
||||
|
||||
func (d *v1Decoder) convertV1Variable(v map[string]any) (Variable, bool) {
|
||||
name := d.readString(v, "name")
|
||||
if name == "" {
|
||||
return Variable{}, false
|
||||
}
|
||||
description := d.readString(v, "description")
|
||||
kind := d.readString(v, "type")
|
||||
|
||||
switch kind {
|
||||
case "TEXTBOX":
|
||||
spec := &TextVariableSpec{
|
||||
Display: Display{Name: name, Description: description},
|
||||
Value: d.readString(v, "textboxValue"),
|
||||
Name: name,
|
||||
}
|
||||
return Variable{Kind: variable.KindText, Spec: spec}, true
|
||||
|
||||
case "QUERY", "CUSTOM", "DYNAMIC":
|
||||
// Drop (don't fail on) a dynamic variable with no attribute — it can't resolve.
|
||||
if kind == "DYNAMIC" && d.readString(v, "dynamicVariablesAttribute") == "" {
|
||||
return Variable{}, false
|
||||
}
|
||||
listSpec := &ListVariableSpec{
|
||||
Display: Display{Name: name, Description: description},
|
||||
AllowAllValue: d.readBool(v, "showALLOption"),
|
||||
AllowMultiple: d.readBool(v, "multiSelect"),
|
||||
CustomAllValue: d.readString(v, "customAllValue"),
|
||||
CapturingRegexp: d.readString(v, "capturingRegexp"),
|
||||
Sort: mapV1Sort(d.readString(v, "sort")),
|
||||
Plugin: d.variablePluginFor(kind, v),
|
||||
Name: name,
|
||||
}
|
||||
if dv := mapV1VariableDefault(v); dv != nil {
|
||||
listSpec.DefaultValue = dv
|
||||
}
|
||||
return Variable{Kind: variable.KindList, Spec: listSpec}, true
|
||||
|
||||
default:
|
||||
d.note("variable %q has unknown type %q", name, kind)
|
||||
return Variable{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) variablePluginFor(kind string, v map[string]any) VariablePlugin {
|
||||
switch kind {
|
||||
case "QUERY":
|
||||
return VariablePlugin{
|
||||
Kind: VariableKindQuery,
|
||||
Spec: &QueryVariableSpec{QueryValue: d.readString(v, "queryValue")},
|
||||
}
|
||||
case "CUSTOM":
|
||||
return VariablePlugin{
|
||||
Kind: VariableKindCustom,
|
||||
Spec: &CustomVariableSpec{CustomValue: d.readString(v, "customValue")},
|
||||
}
|
||||
case "DYNAMIC":
|
||||
spec := &DynamicVariableSpec{Name: d.readString(v, "dynamicVariablesAttribute")}
|
||||
if signal := signalFromDataSource(v["dynamicVariablesSource"]); !signal.IsZero() {
|
||||
spec.Signal = signal
|
||||
}
|
||||
return VariablePlugin{Kind: VariableKindDynamic, Spec: spec}
|
||||
}
|
||||
return VariablePlugin{}
|
||||
}
|
||||
|
||||
// mapV1VariableDefault reads selectedValue/defaultValue, both polymorphic
|
||||
// (string|array), so it indexes the raw value and lets defaultValueFromAny
|
||||
// type-switch — no typed accessor, intentionally lenient.
|
||||
func mapV1VariableDefault(v map[string]any) *VariableDefaultValue {
|
||||
if raw, ok := v["selectedValue"]; ok {
|
||||
return defaultValueFromAny(raw)
|
||||
}
|
||||
if raw, ok := v["defaultValue"]; ok {
|
||||
return defaultValueFromAny(raw)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func defaultValueFromAny(raw any) *VariableDefaultValue {
|
||||
switch v := raw.(type) {
|
||||
case string:
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
return &VariableDefaultValue{variable.DefaultValue{SingleValue: v}}
|
||||
case []any:
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
values := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
if s, ok := item.(string); ok && s != "" {
|
||||
values = append(values, s)
|
||||
}
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &VariableDefaultValue{variable.DefaultValue{SliceValues: values}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapV1Sort(s string) ListVariableSpecSort {
|
||||
switch s {
|
||||
case "ASC":
|
||||
return SortAlphabeticalAsc
|
||||
case "DESC":
|
||||
return SortAlphabeticalDesc
|
||||
}
|
||||
return ListVariableSpecSort{} // zero (omitzero) — SortNone is the implicit default
|
||||
}
|
||||
@@ -27,6 +27,7 @@ type ClusterRecord struct {
|
||||
ClusterMemoryAllocatable float64 `json:"clusterMemoryAllocatable" required:"true"`
|
||||
NodeCountsByReadiness NodeCountsByReadiness `json:"nodeCountsByReadiness" required:"true"`
|
||||
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
|
||||
PodCountsByStatus PodCountsByStatus `json:"podCountsByStatus" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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,12 @@ 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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
// WrapInV5Envelope translates a single v4 builder query/formula map into a
|
||||
// v5 query envelope ({"type": ..., "spec": ...}). It is a pure shape transform
|
||||
// over untyped maps: v4 builder field names (groupBy/orderBy/selectColumns/
|
||||
// dataSource) are rewritten to their v5 equivalents and a `signal` is derived
|
||||
// from the data source. queryType selects the envelope type, except a formula
|
||||
// (detected when name != queryMap["expression"]) is always emitted as
|
||||
// "builder_formula".
|
||||
//
|
||||
// Migration code (pkg/transition) and the v1→v2 dashboard conversion both
|
||||
// produce v5 envelopes, so this lives here with the v5 query types rather than
|
||||
// in an infra-level package.
|
||||
func WrapInV5Envelope(name string, queryMap map[string]any, queryType string) map[string]any {
|
||||
// Create a properly structured v5 query
|
||||
v5Query := map[string]any{
|
||||
"name": name,
|
||||
"disabled": queryMap["disabled"],
|
||||
"legend": queryMap["legend"],
|
||||
}
|
||||
|
||||
if name != queryMap["expression"] {
|
||||
// formula
|
||||
queryType = "builder_formula"
|
||||
v5Query["expression"] = queryMap["expression"]
|
||||
if functions, ok := queryMap["functions"]; ok {
|
||||
v5Query["functions"] = functions
|
||||
}
|
||||
return map[string]any{
|
||||
"type": queryType,
|
||||
"spec": v5Query,
|
||||
}
|
||||
}
|
||||
|
||||
// Add signal based on data source
|
||||
if dataSource, ok := queryMap["dataSource"].(string); ok {
|
||||
switch dataSource {
|
||||
case "traces":
|
||||
v5Query["signal"] = "traces"
|
||||
case "logs":
|
||||
v5Query["signal"] = "logs"
|
||||
case "metrics":
|
||||
v5Query["signal"] = "metrics"
|
||||
}
|
||||
}
|
||||
|
||||
if stepInterval, ok := queryMap["stepInterval"]; ok {
|
||||
v5Query["stepInterval"] = stepInterval
|
||||
}
|
||||
|
||||
if aggregations, ok := queryMap["aggregations"]; ok {
|
||||
v5Query["aggregations"] = aggregations
|
||||
}
|
||||
|
||||
if filter, ok := queryMap["filter"]; ok {
|
||||
v5Query["filter"] = filter
|
||||
}
|
||||
|
||||
// Copy groupBy with proper structure
|
||||
if groupBy, ok := queryMap["groupBy"].([]any); ok {
|
||||
v5GroupBy := make([]any, len(groupBy))
|
||||
for i, gb := range groupBy {
|
||||
if gbMap, ok := gb.(map[string]any); ok {
|
||||
v5GroupBy[i] = map[string]any{
|
||||
"name": gbMap["key"],
|
||||
"fieldDataType": gbMap["dataType"],
|
||||
"fieldContext": gbMap["type"],
|
||||
}
|
||||
}
|
||||
}
|
||||
v5Query["groupBy"] = v5GroupBy
|
||||
}
|
||||
|
||||
// Copy orderBy with proper structure
|
||||
if orderBy, ok := queryMap["orderBy"].([]any); ok {
|
||||
v5OrderBy := make([]any, len(orderBy))
|
||||
for i, ob := range orderBy {
|
||||
if obMap, ok := ob.(map[string]any); ok {
|
||||
v5OrderBy[i] = map[string]any{
|
||||
"key": map[string]any{
|
||||
"name": obMap["columnName"],
|
||||
"fieldDataType": obMap["dataType"],
|
||||
"fieldContext": obMap["type"],
|
||||
},
|
||||
"direction": obMap["order"],
|
||||
}
|
||||
}
|
||||
}
|
||||
v5Query["order"] = v5OrderBy
|
||||
}
|
||||
|
||||
// Copy selectColumns as selectFields
|
||||
if selectColumns, ok := queryMap["selectColumns"].([]any); ok {
|
||||
v5SelectFields := make([]any, len(selectColumns))
|
||||
for i, col := range selectColumns {
|
||||
if colMap, ok := col.(map[string]any); ok {
|
||||
v5SelectFields[i] = map[string]any{
|
||||
"name": colMap["key"],
|
||||
"fieldDataType": colMap["dataType"],
|
||||
"fieldContext": colMap["type"],
|
||||
}
|
||||
}
|
||||
}
|
||||
v5Query["selectFields"] = v5SelectFields
|
||||
}
|
||||
|
||||
// Copy limit and offset
|
||||
if limit, ok := queryMap["limit"]; ok {
|
||||
v5Query["limit"] = limit
|
||||
}
|
||||
if offset, ok := queryMap["offset"]; ok {
|
||||
v5Query["offset"] = offset
|
||||
}
|
||||
|
||||
if having, ok := queryMap["having"]; ok {
|
||||
v5Query["having"] = having
|
||||
}
|
||||
|
||||
if functions, ok := queryMap["functions"]; ok {
|
||||
v5Query["functions"] = functions
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"type": queryType,
|
||||
"spec": v5Query,
|
||||
}
|
||||
}
|
||||
547
tests/fixtures/clickhouse.py
vendored
547
tests/fixtures/clickhouse.py
vendored
@@ -2,7 +2,6 @@ import os
|
||||
from collections.abc import Callable, Generator
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import clickhouse_connect
|
||||
import clickhouse_connect.driver
|
||||
@@ -11,93 +10,37 @@ import docker
|
||||
import docker.errors
|
||||
import pytest
|
||||
from testcontainers.clickhouse import ClickHouseContainer
|
||||
from testcontainers.core.container import DockerContainer, Network
|
||||
from testcontainers.core.container import Network
|
||||
|
||||
from fixtures import reuse, types
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
CLICKHOUSE_USERNAME = "signoz"
|
||||
CLICKHOUSE_PASSWORD = "password"
|
||||
|
||||
CUSTOM_FUNCTION_CONFIG = """
|
||||
<functions>
|
||||
<function>
|
||||
<type>executable</type>
|
||||
<name>histogramQuantile</name>
|
||||
<return_type>Float64</return_type>
|
||||
<argument>
|
||||
<type>Array(Float64)</type>
|
||||
<name>buckets</name>
|
||||
</argument>
|
||||
<argument>
|
||||
<type>Array(Float64)</type>
|
||||
<name>counts</name>
|
||||
</argument>
|
||||
<argument>
|
||||
<type>Float64</type>
|
||||
<name>quantile</name>
|
||||
</argument>
|
||||
<format>CSV</format>
|
||||
<command>./histogramQuantile</command>
|
||||
</function>
|
||||
</functions>
|
||||
"""
|
||||
|
||||
# Distributed inserts to a remote shard are async by default. We force
|
||||
# sycn at the profile level for deterministic tests.
|
||||
CLUSTER_USERS_CONFIG = """
|
||||
<clickhouse>
|
||||
<profiles>
|
||||
<default>
|
||||
<insert_distributed_sync>1</insert_distributed_sync>
|
||||
</default>
|
||||
</profiles>
|
||||
</clickhouse>
|
||||
"""
|
||||
|
||||
|
||||
def render_remote_servers(shard_hosts: list[tuple[str, int]], secret: str | None = None) -> str:
|
||||
"""Render the <remote_servers> block for a cluster named `cluster` with one
|
||||
single-replica shard per (host, port).
|
||||
@pytest.fixture(name="clickhouse", scope="package")
|
||||
def clickhouse(
|
||||
tmpfs: Generator[types.LegacyPath, Any],
|
||||
network: Network,
|
||||
zookeeper: types.TestContainerDocker,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.TestContainerClickhouse:
|
||||
"""
|
||||
Package-scoped fixture for Clickhouse TestContainer.
|
||||
"""
|
||||
shards = "".join(
|
||||
f"""
|
||||
<shard>
|
||||
<replica>
|
||||
<host>{host}</host>
|
||||
<port>{port}</port>
|
||||
</replica>
|
||||
</shard>"""
|
||||
for host, port in shard_hosts
|
||||
)
|
||||
|
||||
# Multi-node clusters need `secret` because distributed queries otherwise
|
||||
# authenticate as the `default` user, which the docker entrypoint restricts
|
||||
# to localhost when a custom user is configured.
|
||||
secret_block = (
|
||||
f"""
|
||||
<secret>{secret}</secret>"""
|
||||
if secret
|
||||
else ""
|
||||
)
|
||||
def create() -> types.TestContainerClickhouse:
|
||||
version = request.config.getoption("--clickhouse-version")
|
||||
|
||||
return f"""
|
||||
<remote_servers>
|
||||
<cluster>{secret_block}{shards}
|
||||
</cluster>
|
||||
</remote_servers>"""
|
||||
container = ClickHouseContainer(
|
||||
image=f"clickhouse/clickhouse-server:{version}",
|
||||
port=9000,
|
||||
username="signoz",
|
||||
password="password",
|
||||
)
|
||||
|
||||
|
||||
def render_node_config(
|
||||
zookeeper_address: str,
|
||||
zookeeper_port: int,
|
||||
shard: str,
|
||||
remote_servers: str,
|
||||
distributed_ddl_path: str = "/clickhouse/task_queue/ddl",
|
||||
) -> str:
|
||||
return f"""
|
||||
cluster_config = f"""
|
||||
<clickhouse>
|
||||
<logger>
|
||||
<level>information</level>
|
||||
@@ -112,23 +55,33 @@ def render_node_config(
|
||||
</logger>
|
||||
|
||||
<macros>
|
||||
<shard>{shard}</shard>
|
||||
<shard>01</shard>
|
||||
<replica>01</replica>
|
||||
</macros>
|
||||
|
||||
<zookeeper>
|
||||
<node>
|
||||
<host>{zookeeper_address}</host>
|
||||
<port>{zookeeper_port}</port>
|
||||
<host>{zookeeper.container_configs["2181"].address}</host>
|
||||
<port>{zookeeper.container_configs["2181"].port}</port>
|
||||
</node>
|
||||
</zookeeper>
|
||||
{remote_servers}
|
||||
|
||||
<remote_servers>
|
||||
<cluster>
|
||||
<shard>
|
||||
<replica>
|
||||
<host>127.0.0.1</host>
|
||||
<port>9000</port>
|
||||
</replica>
|
||||
</shard>
|
||||
</cluster>
|
||||
</remote_servers>
|
||||
|
||||
<user_defined_executable_functions_config>*function.xml</user_defined_executable_functions_config>
|
||||
<user_scripts_path>/var/lib/clickhouse/user_scripts/</user_scripts_path>
|
||||
|
||||
<distributed_ddl>
|
||||
<path>{distributed_ddl_path}</path>
|
||||
<path>/clickhouse/task_queue/ddl</path>
|
||||
<profile>default</profile>
|
||||
</distributed_ddl>
|
||||
|
||||
@@ -169,66 +122,38 @@ def render_node_config(
|
||||
</clickhouse>
|
||||
"""
|
||||
|
||||
custom_function_config = """
|
||||
<functions>
|
||||
<function>
|
||||
<type>executable</type>
|
||||
<name>histogramQuantile</name>
|
||||
<return_type>Float64</return_type>
|
||||
<argument>
|
||||
<type>Array(Float64)</type>
|
||||
<name>buckets</name>
|
||||
</argument>
|
||||
<argument>
|
||||
<type>Array(Float64)</type>
|
||||
<name>counts</name>
|
||||
</argument>
|
||||
<argument>
|
||||
<type>Float64</type>
|
||||
<name>quantile</name>
|
||||
</argument>
|
||||
<format>CSV</format>
|
||||
<command>./histogramQuantile</command>
|
||||
</function>
|
||||
</functions>
|
||||
"""
|
||||
|
||||
def install_histogram_quantile(container: ClickHouseContainer) -> None:
|
||||
wrapped = container.get_wrapped_container()
|
||||
exit_code, output = wrapped.exec_run(
|
||||
[
|
||||
"bash",
|
||||
"-c",
|
||||
(
|
||||
'version="v0.0.1" && '
|
||||
'node_os=$(uname -s | tr "[:upper:]" "[:lower:]") && '
|
||||
"node_arch=$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) && "
|
||||
"cd /tmp && "
|
||||
'wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F${version}/histogram-quantile_${node_os}_${node_arch}.tar.gz" && '
|
||||
"tar -xzf histogram-quantile.tar.gz && "
|
||||
"mkdir -p /var/lib/clickhouse/user_scripts && "
|
||||
"mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile && "
|
||||
"chmod +x /var/lib/clickhouse/user_scripts/histogramQuantile"
|
||||
),
|
||||
],
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise RuntimeError(f"Failed to install histogramQuantile binary: {output.decode()}")
|
||||
|
||||
|
||||
def create_clickhouse( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
tmpfs: Generator[types.LegacyPath, Any],
|
||||
network: Network,
|
||||
keeper: types.TestContainerDocker,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
cache_key: str = "clickhouse",
|
||||
version: str | None = None,
|
||||
) -> types.TestContainerClickhouse:
|
||||
coordinator = next(iter(keeper.container_configs.values()))
|
||||
|
||||
def create() -> types.TestContainerClickhouse:
|
||||
clickhouse_version = version or request.config.getoption("--clickhouse-version")
|
||||
|
||||
container = ClickHouseContainer(
|
||||
image=f"clickhouse/clickhouse-server:{clickhouse_version}",
|
||||
port=9000,
|
||||
username=CLICKHOUSE_USERNAME,
|
||||
password=CLICKHOUSE_PASSWORD,
|
||||
)
|
||||
|
||||
cluster_config = render_node_config(
|
||||
zookeeper_address=coordinator.address,
|
||||
zookeeper_port=coordinator.port,
|
||||
shard="01",
|
||||
remote_servers=render_remote_servers([("127.0.0.1", 9000)]),
|
||||
)
|
||||
|
||||
tmp_dir = tmpfs(cache_key)
|
||||
tmp_dir = tmpfs("clickhouse")
|
||||
cluster_config_file_path = os.path.join(tmp_dir, "cluster.xml")
|
||||
with open(cluster_config_file_path, "w", encoding="utf-8") as f:
|
||||
f.write(cluster_config)
|
||||
|
||||
custom_function_file_path = os.path.join(tmp_dir, "custom-function.xml")
|
||||
with open(custom_function_file_path, "w", encoding="utf-8") as f:
|
||||
f.write(CUSTOM_FUNCTION_CONFIG)
|
||||
f.write(custom_function_config)
|
||||
|
||||
container.with_volume_mapping(cluster_config_file_path, "/etc/clickhouse-server/config.d/cluster.xml")
|
||||
container.with_volume_mapping(
|
||||
@@ -238,7 +163,27 @@ def create_clickhouse( # pylint: disable=too-many-arguments,too-many-positional
|
||||
container.with_network(network)
|
||||
container.start()
|
||||
|
||||
install_histogram_quantile(container)
|
||||
# Download and install the histogramQuantile binary
|
||||
wrapped = container.get_wrapped_container()
|
||||
exit_code, output = wrapped.exec_run(
|
||||
[
|
||||
"bash",
|
||||
"-c",
|
||||
(
|
||||
'version="v0.0.1" && '
|
||||
'node_os=$(uname -s | tr "[:upper:]" "[:lower:]") && '
|
||||
"node_arch=$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) && "
|
||||
"cd /tmp && "
|
||||
'wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F${version}/histogram-quantile_${node_os}_${node_arch}.tar.gz" && '
|
||||
"tar -xzf histogram-quantile.tar.gz && "
|
||||
"mkdir -p /var/lib/clickhouse/user_scripts && "
|
||||
"mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile && "
|
||||
"chmod +x /var/lib/clickhouse/user_scripts/histogramQuantile"
|
||||
),
|
||||
],
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise RuntimeError(f"Failed to install histogramQuantile binary: {output.decode()}")
|
||||
|
||||
connection = clickhouse_connect.get_client(
|
||||
user=container.username,
|
||||
@@ -308,7 +253,7 @@ def create_clickhouse( # pylint: disable=too-many-arguments,too-many-positional
|
||||
return reuse.wrap(
|
||||
request,
|
||||
pytestconfig,
|
||||
cache_key,
|
||||
"clickhouse",
|
||||
empty=lambda: types.TestContainerSQL(
|
||||
container=types.TestContainerDocker(id="", host_configs={}, container_configs={}),
|
||||
conn=None,
|
||||
@@ -320,334 +265,6 @@ def create_clickhouse( # pylint: disable=too-many-arguments,too-many-positional
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="clickhouse", scope="package")
|
||||
def clickhouse(
|
||||
tmpfs: Generator[types.LegacyPath, Any],
|
||||
network: Network,
|
||||
zookeeper: types.TestContainerDocker,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.TestContainerClickhouse:
|
||||
"""
|
||||
Package-scoped fixture for Clickhouse TestContainer.
|
||||
"""
|
||||
return create_clickhouse(
|
||||
tmpfs=tmpfs,
|
||||
network=network,
|
||||
keeper=zookeeper,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
)
|
||||
|
||||
|
||||
def local_series_counts(
|
||||
node_conns: list[clickhouse_connect.driver.client.Client],
|
||||
table: str,
|
||||
metric_name: str,
|
||||
) -> list[int]:
|
||||
"""Distinct series per node via the LOCAL (non-distributed) table."""
|
||||
return [
|
||||
int(
|
||||
conn.query(
|
||||
f"SELECT count(DISTINCT fingerprint) FROM signoz_metrics.{table} WHERE metric_name = %(metric_name)s",
|
||||
parameters={"metric_name": metric_name},
|
||||
).result_rows[0][0]
|
||||
)
|
||||
for conn in node_conns
|
||||
]
|
||||
|
||||
|
||||
def assert_spans_shards(
|
||||
node_conns: list[clickhouse_connect.driver.client.Client],
|
||||
table: str,
|
||||
metric_name: str,
|
||||
total: int,
|
||||
) -> None:
|
||||
"""Guard for distributed tests: a green run on a cluster proves nothing
|
||||
unless the seeded series actually landed on more than one shard."""
|
||||
counts = local_series_counts(node_conns, table, metric_name)
|
||||
assert sum(counts) == total, f"expected {total} series in {table} across shards, got {counts}"
|
||||
assert min(counts) > 0, f"seeded series in {table} all landed on one shard: {counts}"
|
||||
|
||||
|
||||
@pytest.fixture(name="clickhouse_node_conns", scope="function")
|
||||
def clickhouse_node_conns(
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
) -> Generator[list[clickhouse_connect.driver.client.Client], Any]:
|
||||
"""Per-node clients (index 0 = the initiator) for asserting shard-local
|
||||
state via the local, non-distributed tables. Empty for single-node
|
||||
fixtures, which don't populate `nodes`."""
|
||||
conns = [
|
||||
clickhouse_connect.get_client(
|
||||
user=clickhouse.env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_USERNAME"],
|
||||
password=clickhouse.env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_PASSWORD"],
|
||||
host=node.host_configs["8123"].address,
|
||||
port=node.host_configs["8123"].port,
|
||||
)
|
||||
for node in clickhouse.nodes
|
||||
]
|
||||
yield conns
|
||||
for conn in conns:
|
||||
conn.close()
|
||||
|
||||
|
||||
KEEPER_CONFIG = """
|
||||
<clickhouse>
|
||||
<listen_host>0.0.0.0</listen_host>
|
||||
<keeper_server>
|
||||
<tcp_port>9181</tcp_port>
|
||||
<server_id>1</server_id>
|
||||
<log_storage_path>/var/lib/clickhouse-keeper/coordination/log</log_storage_path>
|
||||
<snapshot_storage_path>/var/lib/clickhouse-keeper/coordination/snapshots</snapshot_storage_path>
|
||||
<coordination_settings>
|
||||
<operation_timeout_ms>10000</operation_timeout_ms>
|
||||
<session_timeout_ms>30000</session_timeout_ms>
|
||||
<raft_logs_level>warning</raft_logs_level>
|
||||
</coordination_settings>
|
||||
<raft_configuration>
|
||||
<server>
|
||||
<id>1</id>
|
||||
<hostname>localhost</hostname>
|
||||
<port>9234</port>
|
||||
</server>
|
||||
</raft_configuration>
|
||||
</keeper_server>
|
||||
</clickhouse>
|
||||
"""
|
||||
|
||||
|
||||
def create_clickhouse_keeper(
|
||||
tmpfs: Generator[types.LegacyPath, Any],
|
||||
network: Network,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
cache_key: str = "clickhousekeeper",
|
||||
version: str | None = None,
|
||||
) -> types.TestContainerDocker:
|
||||
|
||||
def create() -> types.TestContainerDocker:
|
||||
keeper_version = version or request.config.getoption("--clickhouse-version")
|
||||
|
||||
tmp_dir = tmpfs(cache_key)
|
||||
keeper_config_file_path = os.path.join(tmp_dir, "keeper_config.xml")
|
||||
with open(keeper_config_file_path, "w", encoding="utf-8") as f:
|
||||
f.write(KEEPER_CONFIG)
|
||||
|
||||
container = DockerContainer(image=f"clickhouse/clickhouse-keeper:{keeper_version}")
|
||||
container.with_volume_mapping(keeper_config_file_path, "/etc/clickhouse-keeper/keeper_config.xml")
|
||||
container.with_exposed_ports(9181)
|
||||
container.with_network(network=network)
|
||||
|
||||
container.start()
|
||||
return types.TestContainerDocker(
|
||||
id=container.get_wrapped_container().id,
|
||||
host_configs={
|
||||
"9181": types.TestContainerUrlConfig(
|
||||
scheme="tcp",
|
||||
address=container.get_container_host_ip(),
|
||||
port=container.get_exposed_port(9181),
|
||||
)
|
||||
},
|
||||
container_configs={
|
||||
"9181": types.TestContainerUrlConfig(
|
||||
scheme="tcp",
|
||||
address=container.get_wrapped_container().name,
|
||||
port=9181,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
def delete(container: types.TestContainerDocker):
|
||||
client = docker.from_env()
|
||||
try:
|
||||
client.containers.get(container_id=container.id).stop()
|
||||
client.containers.get(container_id=container.id).remove(v=True)
|
||||
except docker.errors.NotFound:
|
||||
logger.info(
|
||||
"Skipping removal of ClickHouse Keeper, Keeper(%s) not found. Maybe it was manually removed?",
|
||||
{"id": container.id},
|
||||
)
|
||||
|
||||
def restore(cache: dict) -> types.TestContainerDocker:
|
||||
return types.TestContainerDocker.from_cache(cache)
|
||||
|
||||
return reuse.wrap(
|
||||
request,
|
||||
pytestconfig,
|
||||
cache_key,
|
||||
lambda: types.TestContainerDocker(id="", host_configs={}, container_configs={}),
|
||||
create,
|
||||
delete,
|
||||
restore,
|
||||
)
|
||||
|
||||
|
||||
def create_clickhouse_cluster( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
tmpfs: Generator[types.LegacyPath, Any],
|
||||
network: Network,
|
||||
keeper: types.TestContainerDocker,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
cache_key: str = "clickhouse_cluster",
|
||||
shards: int = 2,
|
||||
version: str | None = None,
|
||||
) -> types.TestContainerClickhouse:
|
||||
"""
|
||||
To some extent, taken inspiration from how ClickHouse's own integration
|
||||
harness composes real clusters: deterministic hostnames
|
||||
(network aliases), per-node shard macros, and a shared cluster definition
|
||||
named `cluster`.
|
||||
|
||||
`conn`/`env` point at node 1 i.e the initiator every query-service query and
|
||||
migration goes through. Per-node containers are exposed via `nodes` so
|
||||
tests can assert shard-local state. `keeper` is any coordination service
|
||||
(ZooKeeper or ClickHouse Keeper).
|
||||
"""
|
||||
coordinator = next(iter(keeper.container_configs.values()))
|
||||
|
||||
def create() -> types.TestContainerClickhouse:
|
||||
clickhouse_version = version or request.config.getoption("--clickhouse-version")
|
||||
|
||||
# Unique aliases per creation: docker allows duplicate network aliases
|
||||
# (DNS round-robin), so a stale cluster must never share names with a
|
||||
# fresh one.
|
||||
suffix = uuid4().hex[:6]
|
||||
aliases = [f"signoz-ch-{suffix}-{i:02d}" for i in range(1, shards + 1)]
|
||||
remote_servers = render_remote_servers([(alias, 9000) for alias in aliases], secret=cache_key)
|
||||
# Own DDL queue path: the keeper instance may be shared with other
|
||||
# environments under --reuse; its DDL queue stays separate.
|
||||
distributed_ddl_path = f"/clickhouse/{cache_key}-{suffix}/task_queue/ddl"
|
||||
|
||||
nodes: list[types.TestContainerDocker] = []
|
||||
started: list[ClickHouseContainer] = []
|
||||
try:
|
||||
for i, alias in enumerate(aliases, start=1):
|
||||
node_config = render_node_config(
|
||||
zookeeper_address=coordinator.address,
|
||||
zookeeper_port=coordinator.port,
|
||||
shard=f"{i:02d}",
|
||||
remote_servers=remote_servers,
|
||||
distributed_ddl_path=distributed_ddl_path,
|
||||
)
|
||||
|
||||
tmp_dir = tmpfs(f"clickhouse-{suffix}-{i:02d}")
|
||||
cluster_config_file_path = os.path.join(tmp_dir, "cluster.xml")
|
||||
with open(cluster_config_file_path, "w", encoding="utf-8") as f:
|
||||
f.write(node_config)
|
||||
custom_function_file_path = os.path.join(tmp_dir, "custom-function.xml")
|
||||
with open(custom_function_file_path, "w", encoding="utf-8") as f:
|
||||
f.write(CUSTOM_FUNCTION_CONFIG)
|
||||
users_config_file_path = os.path.join(tmp_dir, "users.xml")
|
||||
with open(users_config_file_path, "w", encoding="utf-8") as f:
|
||||
f.write(CLUSTER_USERS_CONFIG)
|
||||
|
||||
container = ClickHouseContainer(
|
||||
image=f"clickhouse/clickhouse-server:{clickhouse_version}",
|
||||
port=9000,
|
||||
username=CLICKHOUSE_USERNAME,
|
||||
password=CLICKHOUSE_PASSWORD,
|
||||
)
|
||||
container.with_volume_mapping(cluster_config_file_path, "/etc/clickhouse-server/config.d/cluster.xml")
|
||||
container.with_volume_mapping(custom_function_file_path, "/etc/clickhouse-server/custom-function.xml")
|
||||
container.with_volume_mapping(users_config_file_path, "/etc/clickhouse-server/users.d/integration-cluster.xml")
|
||||
container.with_network(network)
|
||||
container.with_network_aliases(alias)
|
||||
container.start()
|
||||
started.append(container)
|
||||
|
||||
install_histogram_quantile(container)
|
||||
|
||||
nodes.append(
|
||||
types.TestContainerDocker(
|
||||
id=container.get_wrapped_container().id,
|
||||
host_configs={
|
||||
"9000": types.TestContainerUrlConfig(
|
||||
"tcp",
|
||||
container.get_container_host_ip(),
|
||||
container.get_exposed_port(9000),
|
||||
),
|
||||
"8123": types.TestContainerUrlConfig(
|
||||
"tcp",
|
||||
container.get_container_host_ip(),
|
||||
container.get_exposed_port(8123),
|
||||
),
|
||||
},
|
||||
container_configs={
|
||||
"9000": types.TestContainerUrlConfig("tcp", alias, 9000),
|
||||
"8123": types.TestContainerUrlConfig("tcp", alias, 8123),
|
||||
},
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
for container in started:
|
||||
container.stop()
|
||||
raise
|
||||
|
||||
connection = clickhouse_connect.get_client(
|
||||
user=CLICKHOUSE_USERNAME,
|
||||
password=CLICKHOUSE_PASSWORD,
|
||||
host=nodes[0].host_configs["8123"].address,
|
||||
port=nodes[0].host_configs["8123"].port,
|
||||
)
|
||||
|
||||
return types.TestContainerClickhouse(
|
||||
container=nodes[0],
|
||||
conn=connection,
|
||||
env={
|
||||
"SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN": f"tcp://{CLICKHOUSE_USERNAME}:{CLICKHOUSE_PASSWORD}@{aliases[0]}:{9000}",
|
||||
"SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_USERNAME": CLICKHOUSE_USERNAME,
|
||||
"SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_PASSWORD": CLICKHOUSE_PASSWORD,
|
||||
"SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER": "cluster",
|
||||
},
|
||||
nodes=nodes,
|
||||
)
|
||||
|
||||
def delete(resource: types.TestContainerClickhouse) -> None:
|
||||
client = docker.from_env()
|
||||
for node in resource.nodes or [resource.container]:
|
||||
try:
|
||||
client.containers.get(container_id=node.id).stop()
|
||||
client.containers.get(container_id=node.id).remove(v=True)
|
||||
except docker.errors.NotFound:
|
||||
logger.info(
|
||||
"Skipping removal of Clickhouse cluster node, node(%s) not found. Maybe it was manually removed?",
|
||||
{"id": node.id},
|
||||
)
|
||||
|
||||
def restore(cache: dict) -> types.TestContainerClickhouse:
|
||||
nodes = [types.TestContainerDocker.from_cache(node) for node in cache["nodes"]]
|
||||
env = cache["env"]
|
||||
host_config = nodes[0].host_configs["8123"]
|
||||
|
||||
conn = clickhouse_connect.get_client(
|
||||
user=env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_USERNAME"],
|
||||
password=env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_PASSWORD"],
|
||||
host=host_config.address,
|
||||
port=host_config.port,
|
||||
)
|
||||
|
||||
return types.TestContainerClickhouse(
|
||||
container=nodes[0],
|
||||
conn=conn,
|
||||
env=env,
|
||||
nodes=nodes,
|
||||
)
|
||||
|
||||
return reuse.wrap(
|
||||
request,
|
||||
pytestconfig,
|
||||
cache_key,
|
||||
empty=lambda: types.TestContainerClickhouse(
|
||||
container=types.TestContainerDocker(id="", host_configs={}, container_configs={}),
|
||||
conn=None,
|
||||
env={},
|
||||
),
|
||||
create=create,
|
||||
delete=delete,
|
||||
restore=restore,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="check_query_log")
|
||||
def check_query_log(
|
||||
signoz: types.SigNoz,
|
||||
|
||||
29
tests/fixtures/http.py
vendored
29
tests/fixtures/http.py
vendored
@@ -18,22 +18,19 @@ from fixtures.logger import setup_logger
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
ZEUS_NETWORK_ALIAS = "signoz-zeus-it"
|
||||
|
||||
|
||||
def create_zeus(
|
||||
@pytest.fixture(name="zeus", scope="package")
|
||||
def zeus(
|
||||
network: Network,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
cache_key: str = "zeus",
|
||||
alias: str | None = None,
|
||||
) -> types.TestContainerDocker:
|
||||
"""
|
||||
Package-scoped fixture for running zeus
|
||||
"""
|
||||
|
||||
def create() -> types.TestContainerDocker:
|
||||
container = WireMockContainer(image="wiremock/wiremock:2.35.1-1", secure=False)
|
||||
container.with_network(network)
|
||||
if alias:
|
||||
container.with_network_aliases(alias)
|
||||
container.start()
|
||||
|
||||
return types.TestContainerDocker(
|
||||
@@ -45,7 +42,7 @@ def create_zeus(
|
||||
container.get_exposed_port(8080),
|
||||
)
|
||||
},
|
||||
container_configs={"8080": types.TestContainerUrlConfig("http", alias or container.get_wrapped_container().name, 8080)},
|
||||
container_configs={"8080": types.TestContainerUrlConfig("http", container.get_wrapped_container().name, 8080)},
|
||||
)
|
||||
|
||||
def delete(container: types.TestContainerDocker):
|
||||
@@ -65,7 +62,7 @@ def create_zeus(
|
||||
return reuse.wrap(
|
||||
request,
|
||||
pytestconfig,
|
||||
cache_key,
|
||||
"zeus",
|
||||
lambda: types.TestContainerDocker(id="", host_configs={}, container_configs={}),
|
||||
create,
|
||||
delete,
|
||||
@@ -73,18 +70,6 @@ def create_zeus(
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="zeus", scope="package")
|
||||
def zeus(
|
||||
network: Network,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.TestContainerDocker:
|
||||
"""
|
||||
Package-scoped fixture for running zeus
|
||||
"""
|
||||
return create_zeus(network=network, request=request, pytestconfig=pytestconfig)
|
||||
|
||||
|
||||
@pytest.fixture(name="gateway", scope="package")
|
||||
def gateway(
|
||||
network: Network,
|
||||
|
||||
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
|
||||
51
tests/fixtures/metricreduction.py
vendored
51
tests/fixtures/metricreduction.py
vendored
@@ -1,51 +0,0 @@
|
||||
import datetime
|
||||
from collections.abc import Sequence
|
||||
|
||||
from fixtures.metrics import MetricsBufferSample, MetricsBufferTimeSeries
|
||||
|
||||
|
||||
def build_ruled_gauge_buffer(
|
||||
metric_name: str,
|
||||
base_epoch: int,
|
||||
services: Sequence[str],
|
||||
pods_per_service: int,
|
||||
minutes: int,
|
||||
value: float = 1.0,
|
||||
) -> tuple[list[MetricsBufferTimeSeries], list[MetricsBufferSample]]:
|
||||
"""Collector-shaped buffer rows for a gauge under a reduction rule that
|
||||
keeps `service`: per raw series a raw series row (is_reduced=false, full
|
||||
labels, reduced_fingerprint -> group) plus the group's reduced series row
|
||||
(is_reduced=true, kept labels), and one raw sample per series per minute
|
||||
carrying both fingerprints. Returns (time_series, samples) for
|
||||
insert_buffer_metrics."""
|
||||
reduced_series = {
|
||||
service: MetricsBufferTimeSeries(
|
||||
metric_name=metric_name,
|
||||
labels={"service": service},
|
||||
timestamp=datetime.datetime.fromtimestamp(base_epoch, tz=datetime.UTC),
|
||||
is_reduced=True,
|
||||
)
|
||||
for service in services
|
||||
}
|
||||
raw_series = [
|
||||
MetricsBufferTimeSeries(
|
||||
metric_name=metric_name,
|
||||
labels={"service": service, "pod": f"pod-{service}-{i}"},
|
||||
timestamp=datetime.datetime.fromtimestamp(base_epoch, tz=datetime.UTC),
|
||||
reduced_fingerprint=reduced_series[service].fingerprint,
|
||||
)
|
||||
for service in services
|
||||
for i in range(pods_per_service)
|
||||
]
|
||||
samples = [
|
||||
MetricsBufferSample(
|
||||
metric_name=metric_name,
|
||||
fingerprint=ts.fingerprint,
|
||||
timestamp=datetime.datetime.fromtimestamp(base_epoch + minute * 60, tz=datetime.UTC),
|
||||
value=value,
|
||||
reduced_fingerprint=ts.reduced_fingerprint,
|
||||
)
|
||||
for ts in raw_series
|
||||
for minute in range(minutes)
|
||||
]
|
||||
return raw_series + list(reduced_series.values()), samples
|
||||
426
tests/fixtures/metrics.py
vendored
426
tests/fixtures/metrics.py
vendored
@@ -11,14 +11,6 @@ import pytest
|
||||
from fixtures import types
|
||||
from fixtures.time import parse_timestamp
|
||||
|
||||
_REDUCED_METRICS_TABLES_TO_TRUNCATE = [
|
||||
"time_series_v4_reduced",
|
||||
"samples_v4_reduced_last_60s",
|
||||
"samples_v4_reduced_sum_60s",
|
||||
"time_series_v4_buffer",
|
||||
"samples_v4_buffer",
|
||||
]
|
||||
|
||||
|
||||
class MetricsTimeSeries(ABC):
|
||||
"""Represents a row in the time_series_v4 table."""
|
||||
@@ -422,267 +414,6 @@ class Metrics(ABC):
|
||||
return metrics
|
||||
|
||||
|
||||
class MetricsReducedTimeSeries(ABC):
|
||||
"""Represents a row in the time_series_v4_reduced table i.e what
|
||||
the time_series_v4_reduced_mv materializes for a metric under a
|
||||
reduction rule. One row per kept-label group. `fingerprint` holds the
|
||||
reduced fingerprint and `labels` contains only the kept labels.
|
||||
|
||||
The fingerprint recipe (md5, like MetricsTimeSeries) does not match the
|
||||
collector's real hash; it only needs to be consistent with the
|
||||
reduced_fingerprint used in the reduced samples rows.
|
||||
"""
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
self,
|
||||
metric_name: str,
|
||||
kept_labels: dict[str, str],
|
||||
timestamp: datetime.datetime,
|
||||
temporality: str = "Unspecified",
|
||||
description: str = "",
|
||||
unit: str = "",
|
||||
type_: str = "Gauge",
|
||||
is_monotonic: bool = False,
|
||||
env: str = "default",
|
||||
) -> None:
|
||||
kept_labels = dict(kept_labels)
|
||||
kept_labels["__name__"] = metric_name
|
||||
self.env = env
|
||||
# mirror time_series_v4_reduced_mv: monotonic cumulative counters are
|
||||
# reduced as deltas
|
||||
if temporality == "Cumulative" and is_monotonic:
|
||||
temporality = "Delta"
|
||||
self.temporality = temporality
|
||||
self.metric_name = metric_name
|
||||
self.description = description
|
||||
self.unit = unit
|
||||
self.type = type_
|
||||
self.is_monotonic = is_monotonic
|
||||
self.labels = json.dumps(kept_labels, separators=(",", ":"))
|
||||
self.attrs = kept_labels
|
||||
self.unix_milli = np.int64(int(timestamp.timestamp() * 1e3))
|
||||
self.normalized = False
|
||||
|
||||
fingerprint_str = metric_name + self.labels
|
||||
self.fingerprint = np.uint64(int(hashlib.md5(fingerprint_str.encode()).hexdigest()[:16], 16))
|
||||
|
||||
def to_row(self) -> list:
|
||||
return [
|
||||
self.env,
|
||||
self.temporality,
|
||||
self.metric_name,
|
||||
self.description,
|
||||
self.unit,
|
||||
self.type,
|
||||
self.is_monotonic,
|
||||
self.fingerprint,
|
||||
self.unix_milli,
|
||||
self.labels,
|
||||
self.attrs,
|
||||
{},
|
||||
{},
|
||||
self.normalized,
|
||||
]
|
||||
|
||||
|
||||
class MetricsReducedSampleLast60s(ABC):
|
||||
"""Represents a row in the samples_v4_reduced_last_60s table. One 60s
|
||||
bucket per reduced group, as the samples_v4_reduced_last_60s_mv refresh
|
||||
would emit it (gauges and non-monotonic cumulative sums)."""
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
self,
|
||||
metric_name: str,
|
||||
reduced_fingerprint: np.uint64,
|
||||
timestamp: datetime.datetime,
|
||||
sum_last: float,
|
||||
min_value: float,
|
||||
max_value: float,
|
||||
sum_values: float,
|
||||
count_series: int,
|
||||
count_samples: int,
|
||||
temporality: str = "Unspecified",
|
||||
env: str = "default",
|
||||
computed_at: datetime.datetime | None = None,
|
||||
) -> None:
|
||||
self.env = env
|
||||
self.temporality = temporality
|
||||
self.metric_name = metric_name
|
||||
self.reduced_fingerprint = reduced_fingerprint
|
||||
# buckets are 60s-aligned: intDiv(unix_milli, 60000) * 60000
|
||||
self.unix_milli = np.int64((int(timestamp.timestamp() * 1e3) // 60000) * 60000)
|
||||
self.sum_last = np.float64(sum_last)
|
||||
self.min = np.float64(min_value)
|
||||
self.max = np.float64(max_value)
|
||||
self.sum_values = np.float64(sum_values)
|
||||
self.count_series = np.uint64(count_series)
|
||||
self.count_samples = np.uint64(count_samples)
|
||||
# the refresh stamps now(); default to shortly after the bucket closes
|
||||
if computed_at is None:
|
||||
computed_at = datetime.datetime.fromtimestamp(int(self.unix_milli) / 1e3, tz=datetime.UTC) + datetime.timedelta(seconds=180)
|
||||
self.computed_at = computed_at
|
||||
|
||||
def to_row(self) -> list:
|
||||
return [
|
||||
self.env,
|
||||
self.temporality,
|
||||
self.metric_name,
|
||||
self.reduced_fingerprint,
|
||||
self.unix_milli,
|
||||
self.sum_last,
|
||||
self.min,
|
||||
self.max,
|
||||
self.sum_values,
|
||||
self.count_series,
|
||||
self.count_samples,
|
||||
self.computed_at,
|
||||
]
|
||||
|
||||
|
||||
class MetricsReducedSampleSum60s(ABC):
|
||||
"""Represents a row in the samples_v4_reduced_sum_60s table. One 60s
|
||||
bucket per reduced group for delta counters and histograms."""
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
self,
|
||||
metric_name: str,
|
||||
reduced_fingerprint: np.uint64,
|
||||
timestamp: datetime.datetime,
|
||||
sum_value: float,
|
||||
count_series: int,
|
||||
count_samples: int,
|
||||
temporality: str = "Delta",
|
||||
env: str = "default",
|
||||
computed_at: datetime.datetime | None = None,
|
||||
) -> None:
|
||||
self.env = env
|
||||
self.temporality = temporality
|
||||
self.metric_name = metric_name
|
||||
self.reduced_fingerprint = reduced_fingerprint
|
||||
self.unix_milli = np.int64((int(timestamp.timestamp() * 1e3) // 60000) * 60000)
|
||||
self.sum = np.float64(sum_value)
|
||||
self.count_series = np.uint64(count_series)
|
||||
self.count_samples = np.uint64(count_samples)
|
||||
if computed_at is None:
|
||||
computed_at = datetime.datetime.fromtimestamp(int(self.unix_milli) / 1e3, tz=datetime.UTC) + datetime.timedelta(seconds=180)
|
||||
self.computed_at = computed_at
|
||||
|
||||
def to_row(self) -> list:
|
||||
return [
|
||||
self.env,
|
||||
self.temporality,
|
||||
self.metric_name,
|
||||
self.reduced_fingerprint,
|
||||
self.unix_milli,
|
||||
self.sum,
|
||||
self.count_series,
|
||||
self.count_samples,
|
||||
self.computed_at,
|
||||
]
|
||||
|
||||
|
||||
class MetricsBufferTimeSeries(ABC):
|
||||
"""Represents a row in the time_series_v4_buffer table. This is the collector's
|
||||
universal landing target under cardinality control. For a ruled metric the
|
||||
collector writes two rows per series: the raw one (is_reduced=false, full
|
||||
labels, reduced_fingerprint pointing at its group) and the group's reduced
|
||||
one (is_reduced=true, kept labels, fingerprint = reduced fingerprint)."""
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
self,
|
||||
metric_name: str,
|
||||
labels: dict[str, str],
|
||||
timestamp: datetime.datetime,
|
||||
reduced_fingerprint: np.uint64 | int = 0,
|
||||
is_reduced: bool = False,
|
||||
temporality: str = "Unspecified",
|
||||
description: str = "",
|
||||
unit: str = "",
|
||||
type_: str = "Gauge",
|
||||
is_monotonic: bool = False,
|
||||
env: str = "default",
|
||||
) -> None:
|
||||
labels = dict(labels)
|
||||
labels["__name__"] = metric_name
|
||||
self.env = env
|
||||
self.temporality = temporality
|
||||
self.metric_name = metric_name
|
||||
self.description = description
|
||||
self.unit = unit
|
||||
self.type = type_
|
||||
self.is_monotonic = is_monotonic
|
||||
self.reduced_fingerprint = np.uint64(reduced_fingerprint)
|
||||
self.is_reduced = is_reduced
|
||||
self.labels = json.dumps(labels, separators=(",", ":"))
|
||||
self.attrs = labels
|
||||
self.unix_milli = np.int64(int(timestamp.timestamp() * 1e3))
|
||||
self.normalized = False
|
||||
|
||||
fingerprint_str = metric_name + self.labels
|
||||
self.fingerprint = np.uint64(int(hashlib.md5(fingerprint_str.encode()).hexdigest()[:16], 16))
|
||||
|
||||
def to_row(self) -> list:
|
||||
return [
|
||||
self.env,
|
||||
self.temporality,
|
||||
self.metric_name,
|
||||
self.description,
|
||||
self.unit,
|
||||
self.type,
|
||||
self.is_monotonic,
|
||||
self.fingerprint,
|
||||
self.reduced_fingerprint,
|
||||
self.is_reduced,
|
||||
self.unix_milli,
|
||||
self.labels,
|
||||
self.attrs,
|
||||
{},
|
||||
{},
|
||||
self.normalized,
|
||||
]
|
||||
|
||||
|
||||
class MetricsBufferSample(ABC):
|
||||
"""Represents a row in the samples_v4_buffer table. Ruled samples carry
|
||||
the raw fingerprint plus the group's reduced_fingerprint; unruled samples
|
||||
have reduced_fingerprint = 0."""
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
self,
|
||||
metric_name: str,
|
||||
fingerprint: np.uint64,
|
||||
timestamp: datetime.datetime,
|
||||
value: float,
|
||||
reduced_fingerprint: np.uint64 | int = 0,
|
||||
is_monotonic: bool = False,
|
||||
temporality: str = "Unspecified",
|
||||
env: str = "default",
|
||||
flags: int = 0,
|
||||
) -> None:
|
||||
self.env = env
|
||||
self.temporality = temporality
|
||||
self.metric_name = metric_name
|
||||
self.fingerprint = fingerprint
|
||||
self.reduced_fingerprint = np.uint64(reduced_fingerprint)
|
||||
self.is_monotonic = is_monotonic
|
||||
self.unix_milli = np.int64(int(timestamp.timestamp() * 1e3))
|
||||
self.value = np.float64(value)
|
||||
self.flags = np.uint32(flags)
|
||||
|
||||
def to_row(self) -> list:
|
||||
return [
|
||||
self.env,
|
||||
self.temporality,
|
||||
self.metric_name,
|
||||
self.fingerprint,
|
||||
self.reduced_fingerprint,
|
||||
self.is_monotonic,
|
||||
self.unix_milli,
|
||||
self.value,
|
||||
self.flags,
|
||||
]
|
||||
|
||||
|
||||
def insert_metrics_to_clickhouse(conn, metrics: list[Metrics]) -> None:
|
||||
"""
|
||||
Insert metrics into ClickHouse tables.
|
||||
@@ -845,163 +576,6 @@ def insert_metrics(
|
||||
)
|
||||
|
||||
|
||||
def insert_reduced_metrics_to_clickhouse(
|
||||
conn,
|
||||
time_series: list[MetricsReducedTimeSeries],
|
||||
last_samples: list[MetricsReducedSampleLast60s] | None = None,
|
||||
sum_samples: list[MetricsReducedSampleSum60s] | None = None,
|
||||
) -> None:
|
||||
"""Insert reduced series into distributed_time_series_v4_reduced and 60s
|
||||
buckets into the reduced samples tables. These tables exist only when
|
||||
the schema migrator version includes the metrics cardinality-control
|
||||
migration."""
|
||||
if time_series:
|
||||
conn.insert(
|
||||
database="signoz_metrics",
|
||||
table="distributed_time_series_v4_reduced",
|
||||
column_names=[
|
||||
"env",
|
||||
"temporality",
|
||||
"metric_name",
|
||||
"description",
|
||||
"unit",
|
||||
"type",
|
||||
"is_monotonic",
|
||||
"fingerprint",
|
||||
"unix_milli",
|
||||
"labels",
|
||||
"attrs",
|
||||
"scope_attrs",
|
||||
"resource_attrs",
|
||||
"__normalized",
|
||||
],
|
||||
data=[ts.to_row() for ts in time_series],
|
||||
)
|
||||
|
||||
if last_samples:
|
||||
conn.insert(
|
||||
database="signoz_metrics",
|
||||
table="distributed_samples_v4_reduced_last_60s",
|
||||
column_names=[
|
||||
"env",
|
||||
"temporality",
|
||||
"metric_name",
|
||||
"reduced_fingerprint",
|
||||
"unix_milli",
|
||||
"sum_last",
|
||||
"min",
|
||||
"max",
|
||||
"sum_values",
|
||||
"count_series",
|
||||
"count_samples",
|
||||
"computed_at",
|
||||
],
|
||||
data=[sample.to_row() for sample in last_samples],
|
||||
)
|
||||
|
||||
if sum_samples:
|
||||
conn.insert(
|
||||
database="signoz_metrics",
|
||||
table="distributed_samples_v4_reduced_sum_60s",
|
||||
column_names=[
|
||||
"env",
|
||||
"temporality",
|
||||
"metric_name",
|
||||
"reduced_fingerprint",
|
||||
"unix_milli",
|
||||
"sum",
|
||||
"count_series",
|
||||
"count_samples",
|
||||
"computed_at",
|
||||
],
|
||||
data=[sample.to_row() for sample in sum_samples],
|
||||
)
|
||||
|
||||
|
||||
def insert_buffer_metrics_to_clickhouse(
|
||||
conn,
|
||||
time_series: list[MetricsBufferTimeSeries],
|
||||
samples: list[MetricsBufferSample],
|
||||
) -> None:
|
||||
if time_series:
|
||||
conn.insert(
|
||||
database="signoz_metrics",
|
||||
table="distributed_time_series_v4_buffer",
|
||||
column_names=[
|
||||
"env",
|
||||
"temporality",
|
||||
"metric_name",
|
||||
"description",
|
||||
"unit",
|
||||
"type",
|
||||
"is_monotonic",
|
||||
"fingerprint",
|
||||
"reduced_fingerprint",
|
||||
"is_reduced",
|
||||
"unix_milli",
|
||||
"labels",
|
||||
"attrs",
|
||||
"scope_attrs",
|
||||
"resource_attrs",
|
||||
"__normalized",
|
||||
],
|
||||
data=[ts.to_row() for ts in time_series],
|
||||
)
|
||||
|
||||
if samples:
|
||||
conn.insert(
|
||||
database="signoz_metrics",
|
||||
table="distributed_samples_v4_buffer",
|
||||
column_names=[
|
||||
"env",
|
||||
"temporality",
|
||||
"metric_name",
|
||||
"fingerprint",
|
||||
"reduced_fingerprint",
|
||||
"is_monotonic",
|
||||
"unix_milli",
|
||||
"value",
|
||||
"flags",
|
||||
],
|
||||
data=[sample.to_row() for sample in samples],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="insert_reduced_metrics", scope="function")
|
||||
def insert_reduced_metrics(
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
) -> Generator[Callable[..., None], Any]:
|
||||
def _insert_reduced_metrics(
|
||||
time_series: list[MetricsReducedTimeSeries],
|
||||
last_samples: list[MetricsReducedSampleLast60s] | None = None,
|
||||
sum_samples: list[MetricsReducedSampleSum60s] | None = None,
|
||||
) -> None:
|
||||
insert_reduced_metrics_to_clickhouse(clickhouse.conn, time_series, last_samples, sum_samples)
|
||||
|
||||
yield _insert_reduced_metrics
|
||||
|
||||
cluster = clickhouse.env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER"]
|
||||
for table in _REDUCED_METRICS_TABLES_TO_TRUNCATE:
|
||||
clickhouse.conn.query(f"TRUNCATE TABLE signoz_metrics.{table} ON CLUSTER '{cluster}' SYNC")
|
||||
|
||||
|
||||
@pytest.fixture(name="insert_buffer_metrics", scope="function")
|
||||
def insert_buffer_metrics(
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
) -> Generator[Callable[..., None], Any]:
|
||||
def _insert_buffer_metrics(
|
||||
time_series: list[MetricsBufferTimeSeries],
|
||||
samples: list[MetricsBufferSample],
|
||||
) -> None:
|
||||
insert_buffer_metrics_to_clickhouse(clickhouse.conn, time_series, samples)
|
||||
|
||||
yield _insert_buffer_metrics
|
||||
|
||||
cluster = clickhouse.env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER"]
|
||||
for table in _REDUCED_METRICS_TABLES_TO_TRUNCATE:
|
||||
clickhouse.conn.query(f"TRUNCATE TABLE signoz_metrics.{table} ON CLUSTER '{cluster}' SYNC")
|
||||
|
||||
|
||||
@pytest.fixture(name="remove_metrics_ttl_and_storage_settings", scope="function")
|
||||
def remove_metrics_ttl_and_storage_settings(signoz: types.SigNoz):
|
||||
"""
|
||||
|
||||
13
tests/fixtures/migrator.py
vendored
13
tests/fixtures/migrator.py
vendored
@@ -8,30 +8,27 @@ from fixtures.logger import setup_logger
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def create_migrator( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
def create_migrator(
|
||||
network: Network,
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
cache_key: str = "migrator",
|
||||
env_overrides: dict | None = None,
|
||||
version: str | None = None,
|
||||
) -> types.Operation:
|
||||
"""
|
||||
Factory function for running schema migrations.
|
||||
Accepts optional env_overrides to customize the migrator environment, and
|
||||
an optional version to pin a schema-migrator release different from the
|
||||
--schema-migrator-version option.
|
||||
Accepts optional env_overrides to customize the migrator environment.
|
||||
"""
|
||||
|
||||
def create() -> None:
|
||||
migrator_version = version or request.config.getoption("--schema-migrator-version")
|
||||
version = request.config.getoption("--schema-migrator-version")
|
||||
client = docker.from_env()
|
||||
|
||||
environment = dict(env_overrides) if env_overrides else {}
|
||||
|
||||
container = client.containers.run(
|
||||
image=f"signoz/signoz-schema-migrator:{migrator_version}",
|
||||
image=f"signoz/signoz-schema-migrator:{version}",
|
||||
command=f"sync --replication=true --cluster-name=cluster --up= --dsn={clickhouse.env['SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN']}",
|
||||
detach=True,
|
||||
auto_remove=False,
|
||||
@@ -50,7 +47,7 @@ def create_migrator( # pylint: disable=too-many-arguments,too-many-positional-a
|
||||
container.remove()
|
||||
|
||||
container = client.containers.run(
|
||||
image=f"signoz/signoz-schema-migrator:{migrator_version}",
|
||||
image=f"signoz/signoz-schema-migrator:{version}",
|
||||
command=f"async --replication=true --cluster-name=cluster --up= --dsn={clickhouse.env['SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN']}",
|
||||
detach=True,
|
||||
auto_remove=False,
|
||||
|
||||
29
tests/fixtures/querier.py
vendored
29
tests/fixtures/querier.py
vendored
@@ -189,35 +189,6 @@ def make_query_request(
|
||||
)
|
||||
|
||||
|
||||
def aligned_epoch(ago: timedelta, step_seconds: int = DEFAULT_STEP_INTERVAL) -> int:
|
||||
"""Epoch seconds for `now - ago`, floored to a step boundary so seeded
|
||||
points land exactly on the query's toStartOfInterval buckets."""
|
||||
return (int((datetime.now(tz=UTC) - ago).timestamp()) // step_seconds) * step_seconds
|
||||
|
||||
|
||||
def query_metric_values( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
signoz: types.SigNoz,
|
||||
token: str,
|
||||
metric_name: str,
|
||||
start_epoch: int,
|
||||
end_epoch: int,
|
||||
time_agg: str,
|
||||
space_agg: str,
|
||||
step_interval: int = DEFAULT_STEP_INTERVAL,
|
||||
) -> list[dict]:
|
||||
"""Run a single metrics builder query over [start_epoch, end_epoch) in
|
||||
epoch seconds and return its series values sorted by timestamp."""
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_epoch * 1000,
|
||||
end_ms=end_epoch * 1000,
|
||||
queries=[build_builder_query("A", metric_name, time_agg, space_agg, step_interval=step_interval)],
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
return sorted(get_series_values(response.json(), "A"), key=lambda v: v["timestamp"])
|
||||
|
||||
|
||||
def build_builder_query(
|
||||
name: str,
|
||||
metric_name: str,
|
||||
|
||||
7
tests/fixtures/types.py
vendored
7
tests/fixtures/types.py
vendored
@@ -1,4 +1,4 @@
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
from urllib.parse import urljoin
|
||||
|
||||
@@ -84,16 +84,11 @@ class TestContainerClickhouse:
|
||||
container: TestContainerDocker
|
||||
conn: clickhouse_connect.driver.client.Client
|
||||
env: dict[str, str]
|
||||
# Per-node containers when running a multi-node cluster. Empty for the
|
||||
# default single-node setup; nodes[0] is the node `conn`/`env` point at
|
||||
# (the initiator every query goes through).
|
||||
nodes: list[TestContainerDocker] = field(default_factory=list)
|
||||
|
||||
def __cache__(self) -> dict:
|
||||
return {
|
||||
"container": self.container.__cache__(),
|
||||
"env": self.env,
|
||||
"nodes": [node.__cache__() for node in self.nodes],
|
||||
}
|
||||
|
||||
def __log__(self) -> str:
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import clickhouse_connect.driver.client
|
||||
|
||||
from fixtures import types
|
||||
|
||||
TOTAL_ROWS = 64
|
||||
|
||||
|
||||
def test_topology(
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
clickhouse_node_conns: list[clickhouse_connect.driver.client.Client],
|
||||
) -> None:
|
||||
aliases = {node.container_configs["9000"].address for node in clickhouse.nodes}
|
||||
|
||||
# Every node sees the same 2-shard cluster definition and identifies
|
||||
# exactly itself as the local replica
|
||||
|
||||
for i, conn in enumerate(clickhouse_node_conns, start=1):
|
||||
rows = conn.query("SELECT shard_num, host_name, is_local FROM system.clusters WHERE cluster = 'cluster' ORDER BY shard_num").result_rows
|
||||
assert [row[0] for row in rows] == [1, 2], f"node {i}: expected 2 shards, got {rows}"
|
||||
assert {row[1] for row in rows} == aliases, f"node {i}: cluster hosts {rows} != node aliases {aliases}"
|
||||
local = [row[0] for row in rows if row[2]]
|
||||
assert local == [i], f"node {i}: expected to be local for shard {i} only, got {local}"
|
||||
|
||||
|
||||
def test_replicated_distributed_round_trip(
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
clickhouse_node_conns: list[clickhouse_connect.driver.client.Client],
|
||||
) -> None:
|
||||
# ON CLUSTER DDL reaches both nodes, Replicated engines register with the
|
||||
# keeper via per-node macros, and a sharded Distributed insert scatters rows
|
||||
# across shards while the distributed read returns the union.
|
||||
conn = clickhouse.conn
|
||||
try:
|
||||
conn.query("CREATE DATABASE IF NOT EXISTS it_cluster ON CLUSTER 'cluster'")
|
||||
conn.query("CREATE TABLE it_cluster.events ON CLUSTER 'cluster' (id UInt64, payload String) ENGINE = ReplicatedMergeTree ORDER BY id")
|
||||
conn.query("CREATE TABLE it_cluster.distributed_events ON CLUSTER 'cluster' AS it_cluster.events ENGINE = Distributed('cluster', 'it_cluster', 'events', cityHash64(id))")
|
||||
|
||||
conn.insert(
|
||||
database="it_cluster",
|
||||
table="distributed_events",
|
||||
column_names=["id", "payload"],
|
||||
data=[[i, f"payload-{i:03d}"] for i in range(TOTAL_ROWS)],
|
||||
)
|
||||
|
||||
distributed_count = int(conn.query("SELECT count() FROM it_cluster.distributed_events").result_rows[0][0])
|
||||
assert distributed_count == TOTAL_ROWS
|
||||
|
||||
local_counts = [int(node_conn.query("SELECT count() FROM it_cluster.events").result_rows[0][0]) for node_conn in clickhouse_node_conns]
|
||||
assert sum(local_counts) == TOTAL_ROWS, f"local counts {local_counts} do not add up to {TOTAL_ROWS}"
|
||||
assert min(local_counts) > 0, f"all rows landed on one shard: {local_counts}"
|
||||
finally:
|
||||
conn.query("DROP DATABASE IF EXISTS it_cluster ON CLUSTER 'cluster' SYNC")
|
||||
@@ -1,47 +0,0 @@
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from testcontainers.core.container import Network
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.clickhouse import create_clickhouse_cluster, create_clickhouse_keeper
|
||||
|
||||
CLICKHOUSE_VERSION = "25.12.5"
|
||||
|
||||
|
||||
@pytest.fixture(name="keeper", scope="package")
|
||||
def keeper_cluster(
|
||||
tmpfs: Generator[types.LegacyPath, Any],
|
||||
network: Network,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.TestContainerDocker:
|
||||
return create_clickhouse_keeper(
|
||||
tmpfs=tmpfs,
|
||||
network=network,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="keeper_cluster",
|
||||
version=CLICKHOUSE_VERSION,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="clickhouse", scope="package")
|
||||
def clickhouse_cluster(
|
||||
tmpfs: Generator[types.LegacyPath, Any],
|
||||
network: Network,
|
||||
keeper: types.TestContainerDocker,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.TestContainerClickhouse:
|
||||
return create_clickhouse_cluster(
|
||||
tmpfs=tmpfs,
|
||||
network=network,
|
||||
keeper=keeper,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="clickhouse_cluster",
|
||||
shards=2,
|
||||
version=CLICKHOUSE_VERSION,
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -404,6 +405,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": {
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import clickhouse_connect.driver.client
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.clickhouse import assert_spans_shards
|
||||
from fixtures.metrics import (
|
||||
Metrics,
|
||||
MetricsReducedSampleLast60s,
|
||||
MetricsReducedTimeSeries,
|
||||
)
|
||||
from fixtures.querier import aligned_epoch, query_metric_values
|
||||
|
||||
|
||||
def test_stitch_across_epoch(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
insert_reduced_metrics: Callable[..., None],
|
||||
clickhouse_node_conns: list[clickhouse_connect.driver.client.Client],
|
||||
) -> None:
|
||||
"""Before the rule activates, samples live in the raw tables; after, only
|
||||
the reduced 60s tables have data. One query spanning the boundary must
|
||||
stitch the two branches into a continuous series with no gap and no double
|
||||
counting: 32 raw series at 2.0 collapse into 16 groups whose sum_last is
|
||||
4.0, so the summed value stays 320 per step across the epoch. Enough
|
||||
series to guarantee both shards hold data (guarded below), so the totals
|
||||
also prove the raw and reduced joins execute shard-local."""
|
||||
metric_name = "test_reduction_stitch"
|
||||
base_epoch = aligned_epoch(timedelta(hours=30), step_seconds=300)
|
||||
services = [f"svc-{i:02d}" for i in range(16)]
|
||||
|
||||
# first 30 minutes: raw samples (2 pods per service, one sample per minute)
|
||||
insert_metrics(
|
||||
[
|
||||
Metrics(
|
||||
metric_name=metric_name,
|
||||
labels={"service": service, "pod": f"{service}-pod-{pod}"},
|
||||
timestamp=datetime.fromtimestamp(base_epoch + minute * 60, tz=UTC),
|
||||
value=2.0,
|
||||
type_="Gauge",
|
||||
is_monotonic=False,
|
||||
)
|
||||
for service in services
|
||||
for pod in range(2)
|
||||
for minute in range(30)
|
||||
]
|
||||
)
|
||||
|
||||
# next 30 minutes: reduced 60s buckets (one group per service)
|
||||
time_series = [
|
||||
MetricsReducedTimeSeries(
|
||||
metric_name=metric_name,
|
||||
kept_labels={"service": service},
|
||||
timestamp=datetime.fromtimestamp(base_epoch + 30 * 60, tz=UTC),
|
||||
)
|
||||
for service in services
|
||||
]
|
||||
insert_reduced_metrics(
|
||||
time_series,
|
||||
[
|
||||
MetricsReducedSampleLast60s(
|
||||
metric_name=metric_name,
|
||||
reduced_fingerprint=ts.fingerprint,
|
||||
timestamp=datetime.fromtimestamp(base_epoch + (30 + minute) * 60, tz=UTC),
|
||||
sum_last=4.0,
|
||||
min_value=2.0,
|
||||
max_value=2.0,
|
||||
sum_values=4.0,
|
||||
count_series=2,
|
||||
count_samples=2,
|
||||
)
|
||||
for ts in time_series
|
||||
for minute in range(30)
|
||||
],
|
||||
)
|
||||
|
||||
assert_spans_shards(clickhouse_node_conns, "time_series_v4", metric_name, total=len(services) * 2)
|
||||
assert_spans_shards(clickhouse_node_conns, "time_series_v4_reduced", metric_name, total=len(services))
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
values = query_metric_values(signoz, token, metric_name, base_epoch, base_epoch + 3600, "sum", "sum", step_interval=300)
|
||||
|
||||
assert [v["timestamp"] for v in values] == [(base_epoch + step * 300) * 1000 for step in range(12)]
|
||||
assert [v["value"] for v in values] == [320.0] * 12
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"space_agg, expected",
|
||||
[
|
||||
("sum", 12.0), # sum_last: 4 + 8
|
||||
("avg", 3.0), # sum(sum_last) / sum(count_series): 12 / 4
|
||||
("min", 1.0), # min(min)
|
||||
("max", 6.0), # max(max)
|
||||
],
|
||||
)
|
||||
def test_space_aggregations(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_reduced_metrics: Callable[..., None],
|
||||
space_agg: str,
|
||||
expected: float,
|
||||
) -> None:
|
||||
"""Space aggregations read the reduced pre-aggregated columns: sum/avg
|
||||
from sum_last with the count_series weight, min/max from the min/max
|
||||
columns."""
|
||||
metric_name = f"test_reduction_space_{space_agg}"
|
||||
base_epoch = aligned_epoch(timedelta(hours=30), step_seconds=300)
|
||||
|
||||
groups = [
|
||||
# (service, sum_last, min, max, count_series)
|
||||
("a", 4.0, 1.0, 3.0, 2),
|
||||
("b", 8.0, 2.0, 6.0, 2),
|
||||
]
|
||||
time_series = {
|
||||
service: MetricsReducedTimeSeries(
|
||||
metric_name=metric_name,
|
||||
kept_labels={"service": service},
|
||||
timestamp=datetime.fromtimestamp(base_epoch, tz=UTC),
|
||||
)
|
||||
for service, _, _, _, _ in groups
|
||||
}
|
||||
insert_reduced_metrics(
|
||||
list(time_series.values()),
|
||||
[
|
||||
MetricsReducedSampleLast60s(
|
||||
metric_name=metric_name,
|
||||
reduced_fingerprint=time_series[service].fingerprint,
|
||||
timestamp=datetime.fromtimestamp(base_epoch + minute * 60, tz=UTC),
|
||||
sum_last=sum_last,
|
||||
min_value=min_value,
|
||||
max_value=max_value,
|
||||
sum_values=sum_last,
|
||||
count_series=count_series,
|
||||
count_samples=count_series,
|
||||
)
|
||||
for service, sum_last, min_value, max_value, count_series in groups
|
||||
for minute in range(20)
|
||||
],
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
values = query_metric_values(signoz, token, metric_name, base_epoch, base_epoch + 20 * 60, "avg", space_agg, step_interval=300)
|
||||
|
||||
assert [v["timestamp"] for v in values] == [(base_epoch + step * 300) * 1000 for step in range(4)]
|
||||
assert [v["value"] for v in values] == [expected] * 4
|
||||
|
||||
|
||||
def test_dedup_latest_computed_at_wins(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_reduced_metrics: Callable[..., None],
|
||||
) -> None:
|
||||
"""The refreshable MVs re-emit every bucket on each refresh with a newer
|
||||
computed_at (APPEND mode); reads must dedup to the latest version per
|
||||
(series, bucket). Recompute the same buckets with a newer computed_at and
|
||||
a different value: only the newer value may be counted."""
|
||||
metric_name = "test_reduction_dedup"
|
||||
base_epoch = aligned_epoch(timedelta(hours=30), step_seconds=300)
|
||||
|
||||
time_series = [
|
||||
MetricsReducedTimeSeries(
|
||||
metric_name=metric_name,
|
||||
kept_labels={"service": service},
|
||||
timestamp=datetime.fromtimestamp(base_epoch, tz=UTC),
|
||||
)
|
||||
for service in ("a", "b")
|
||||
]
|
||||
|
||||
def buckets(sum_last: float, computed_at_offset_seconds: int) -> list[MetricsReducedSampleLast60s]:
|
||||
return [
|
||||
MetricsReducedSampleLast60s(
|
||||
metric_name=metric_name,
|
||||
reduced_fingerprint=ts.fingerprint,
|
||||
timestamp=datetime.fromtimestamp(base_epoch + minute * 60, tz=UTC),
|
||||
sum_last=sum_last,
|
||||
min_value=sum_last,
|
||||
max_value=sum_last,
|
||||
sum_values=sum_last,
|
||||
count_series=1,
|
||||
count_samples=1,
|
||||
computed_at=datetime.fromtimestamp(base_epoch + minute * 60 + computed_at_offset_seconds, tz=UTC),
|
||||
)
|
||||
for ts in time_series
|
||||
for minute in range(10)
|
||||
]
|
||||
|
||||
# first refresh emits 1.0; a later refresh recomputes the same buckets to 5.0
|
||||
insert_reduced_metrics(time_series, buckets(sum_last=1.0, computed_at_offset_seconds=120))
|
||||
insert_reduced_metrics(time_series, buckets(sum_last=5.0, computed_at_offset_seconds=180))
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
values = query_metric_values(signoz, token, metric_name, base_epoch, base_epoch + 10 * 60, "sum", "sum", step_interval=300)
|
||||
|
||||
# 2 groups x 5 buckets x 5.0 per step; 1.0 rows must not contribute
|
||||
assert [v["timestamp"] for v in values] == [(base_epoch + step * 300) * 1000 for step in range(2)]
|
||||
assert [v["value"] for v in values] == [50.0] * 2
|
||||
@@ -1,70 +0,0 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import (
|
||||
MetricsReducedSampleSum60s,
|
||||
MetricsReducedTimeSeries,
|
||||
)
|
||||
from fixtures.querier import aligned_epoch, query_metric_values
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"time_agg, expected",
|
||||
[
|
||||
# 2 groups x 5 buckets x 30.0 per 300s step
|
||||
("rate", 1.0), # 300 / 300s
|
||||
("increase", 300.0),
|
||||
],
|
||||
)
|
||||
def test_counter_rate_and_increase(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_reduced_metrics: Callable[..., None],
|
||||
time_agg: str,
|
||||
expected: float,
|
||||
) -> None:
|
||||
metric_name = f"test_reduction_counter_{time_agg}"
|
||||
base_epoch = aligned_epoch(timedelta(hours=30), step_seconds=300)
|
||||
|
||||
# monotonic cumulative counter: MetricsReducedTimeSeries mirrors the
|
||||
# collector's temporality rewrite to Delta
|
||||
time_series = [
|
||||
MetricsReducedTimeSeries(
|
||||
metric_name=metric_name,
|
||||
kept_labels={"service": service},
|
||||
timestamp=datetime.fromtimestamp(base_epoch, tz=UTC),
|
||||
temporality="Cumulative",
|
||||
type_="Sum",
|
||||
is_monotonic=True,
|
||||
)
|
||||
for service in ("a", "b")
|
||||
]
|
||||
assert all(ts.temporality == "Delta" for ts in time_series)
|
||||
|
||||
insert_reduced_metrics(
|
||||
time_series,
|
||||
sum_samples=[
|
||||
MetricsReducedSampleSum60s(
|
||||
metric_name=metric_name,
|
||||
reduced_fingerprint=ts.fingerprint,
|
||||
timestamp=datetime.fromtimestamp(base_epoch + minute * 60, tz=UTC),
|
||||
sum_value=30.0,
|
||||
count_series=2,
|
||||
count_samples=2,
|
||||
temporality="Delta",
|
||||
)
|
||||
for ts in time_series
|
||||
for minute in range(20)
|
||||
],
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
values = query_metric_values(signoz, token, metric_name, base_epoch, base_epoch + 20 * 60, time_agg, "sum", step_interval=300)
|
||||
|
||||
assert [v["timestamp"] for v in values] == [(base_epoch + step * 300) * 1000 for step in range(4)]
|
||||
assert [v["value"] for v in values] == [expected] * 4
|
||||
@@ -1,70 +0,0 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metricreduction import build_ruled_gauge_buffer
|
||||
from fixtures.querier import (
|
||||
aligned_epoch,
|
||||
build_builder_query,
|
||||
get_all_series,
|
||||
index_series_by_label,
|
||||
make_query_request,
|
||||
query_metric_values,
|
||||
)
|
||||
|
||||
SERVICES = ("a", "b")
|
||||
PODS_PER_SERVICE = 2
|
||||
MINUTES = 20
|
||||
|
||||
|
||||
def test_recent_window_reads_buffer_totals(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_buffer_metrics: Callable[..., None],
|
||||
) -> None:
|
||||
metric_name = "test_reduction_buffer_totals"
|
||||
# samples span [now-25m, now-5m); the query window sits inside the last 24h
|
||||
base_epoch = aligned_epoch(timedelta(minutes=25), step_seconds=300)
|
||||
insert_buffer_metrics(*build_ruled_gauge_buffer(metric_name, base_epoch, SERVICES, PODS_PER_SERVICE, MINUTES))
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
values = query_metric_values(signoz, token, metric_name, base_epoch, base_epoch + MINUTES * 60, "sum", "sum", step_interval=300)
|
||||
|
||||
# 4 raw series x 5 samples x 1.0 per step: full raw resolution, and the
|
||||
# is_reduced=true series rows must not join in (their fingerprints match
|
||||
# no samples, and the ts CTE filters them out)
|
||||
assert [v["timestamp"] for v in values] == [(base_epoch + step * 300) * 1000 for step in range(4)]
|
||||
assert [v["value"] for v in values] == [float(len(SERVICES) * PODS_PER_SERVICE * 5)] * 4
|
||||
|
||||
|
||||
def test_recent_window_group_by_raw_label(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_buffer_metrics: Callable[..., None],
|
||||
) -> None:
|
||||
"""Group-by resolves against the raw buffer series rows (full labels), so
|
||||
grouping by the kept label still sees every raw series underneath."""
|
||||
metric_name = "test_reduction_buffer_groupby"
|
||||
base_epoch = aligned_epoch(timedelta(minutes=25), step_seconds=300)
|
||||
insert_buffer_metrics(*build_ruled_gauge_buffer(metric_name, base_epoch, SERVICES, PODS_PER_SERVICE, MINUTES))
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=base_epoch * 1000,
|
||||
end_ms=(base_epoch + MINUTES * 60) * 1000,
|
||||
queries=[build_builder_query("A", metric_name, "sum", "sum", step_interval=300, group_by=["service"])],
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
series_by_service = index_series_by_label(get_all_series(response.json(), "A"), "service")
|
||||
assert set(series_by_service.keys()) == set(SERVICES)
|
||||
for service in SERVICES:
|
||||
values = sorted(series_by_service[service]["values"], key=lambda v: v["timestamp"])
|
||||
# 2 pods x 5 samples x 1.0 per step
|
||||
assert [v["value"] for v in values] == [float(PODS_PER_SERVICE * 5)] * 4
|
||||
@@ -1,114 +0,0 @@
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from testcontainers.core.container import Network
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import register_admin
|
||||
from fixtures.clickhouse import create_clickhouse_cluster, create_clickhouse_keeper
|
||||
from fixtures.http import ZEUS_NETWORK_ALIAS, create_zeus
|
||||
from fixtures.migrator import create_migrator
|
||||
from fixtures.signoz import create_signoz
|
||||
|
||||
SCHEMA_MIGRATOR_VERSION = "v0.144.6-rc.2"
|
||||
CLICKHOUSE_VERSION = "25.12.5"
|
||||
|
||||
|
||||
@pytest.fixture(name="keeper", scope="package")
|
||||
def keeper_metricreduction(
|
||||
tmpfs: Generator[types.LegacyPath, Any],
|
||||
network: Network,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.TestContainerDocker:
|
||||
return create_clickhouse_keeper(
|
||||
tmpfs=tmpfs,
|
||||
network=network,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="keeper_metricreduction",
|
||||
version=CLICKHOUSE_VERSION,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="zeus", scope="package")
|
||||
def zeus_metricreduction(
|
||||
network: Network,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.TestContainerDocker:
|
||||
return create_zeus(
|
||||
network=network,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="zeus_metricreduction",
|
||||
alias=ZEUS_NETWORK_ALIAS,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="clickhouse", scope="package")
|
||||
def clickhouse_metricreduction(
|
||||
tmpfs: Generator[types.LegacyPath, Any],
|
||||
network: Network,
|
||||
keeper: types.TestContainerDocker,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.TestContainerClickhouse:
|
||||
return create_clickhouse_cluster(
|
||||
tmpfs=tmpfs,
|
||||
network=network,
|
||||
keeper=keeper,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="clickhouse_metricreduction",
|
||||
shards=2,
|
||||
version=CLICKHOUSE_VERSION,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="migrator", scope="package")
|
||||
def migrator_metricreduction(
|
||||
network: Network,
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.Operation:
|
||||
return create_migrator(
|
||||
network=network,
|
||||
clickhouse=clickhouse,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="migrator_metricreduction",
|
||||
version=SCHEMA_MIGRATOR_VERSION,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="signoz", scope="package")
|
||||
def signoz_metricreduction( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
network: Network,
|
||||
zeus: types.TestContainerDocker,
|
||||
gateway: types.TestContainerDocker,
|
||||
sqlstore: types.TestContainerSQL,
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.SigNoz:
|
||||
return create_signoz(
|
||||
network=network,
|
||||
zeus=zeus,
|
||||
gateway=gateway,
|
||||
sqlstore=sqlstore,
|
||||
clickhouse=clickhouse,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="signoz_metricreduction",
|
||||
env_overrides={
|
||||
"SIGNOZ_FLAGGER_CONFIG_BOOLEAN_ENABLE__METRICS__REDUCTION": True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="create_user_admin", scope="package")
|
||||
def create_user_admin_metricreduction(signoz: types.SigNoz, request: pytest.FixtureRequest, pytestconfig: pytest.Config) -> types.Operation:
|
||||
return register_admin(signoz, request, pytestconfig, cache_key="create_user_admin_metricreduction")
|
||||
Reference in New Issue
Block a user