Compare commits

..

1 Commits

Author SHA1 Message Date
srikanthccv
2ab6eafb88 feat(prometheus): add the Prometheus query API under a /prometheus prefix
New GET|POST /prometheus/api/v1/query_range and /prometheus/api/v1/query
endpoints (pkg/prometheus/promapi), following the Prometheus HTTP API:
float-unix or RFC3339 times, float-seconds or duration-string durations,
the {status, data, errorType, error, warnings, infos} envelope with
Prometheus' status codes, and the 11,000-point cap. Wired through
signoz.Handlers like the other domain handlers. Range queries serve
through the RangeExecutor capability when the provider has it, so a
clickhousev2-serving deployment transpiles through these endpoints too.

The promapiconformance suite replays the frozen promqltest corpus against
these endpoints with clickhousev2 as the serving provider - the two paths
nothing else exercises. Instant cases go through /query with a real time
parameter. Its ledger holds the seven known Kahan-class divergences of
transpiled coarse-step serving.

Purely additive: the existing GET /api/v1/query_range and /api/v1/query
handlers are untouched.

Assisted-by: Claude Fable 5
2026-08-07 20:41:07 +05:30
153 changed files with 3432 additions and 8516 deletions

View File

@@ -1,19 +0,0 @@
---
paths:
- "tests/**/*.py"
---
# pytest conventions
For the Python integration suite under `tests/`. Setup, running, and suite layout live in [`docs/contributing/tests/integration.md`](../../docs/contributing/tests/integration.md).
- **No `_`-prefixed helper functions in test modules — this is the rule that matters most.** A reader must be able to see what a test does in its body alone, without chasing private helpers that scatter the meaning across the file. Inline the logic: an expression, a comprehension, a few repeated lines are all fine — repetition across tests is cheaper than indirection. When several tests genuinely share non-trivial setup or assertions, that is what fixtures are for — in `tests/fixtures/`, see the next rule. A module-level `_helper()` is never the answer.
- **Fixtures live in `tests/fixtures/` — never under `integration/tests/`.** Not in test modules, not in suite `conftest.py` files. `tests/fixtures/` is the shared library (auth, signoz, clickhouse, logs/metrics/traces seeding, …): reuse what's there before writing anything new; when a new fixture is genuinely needed, add it to the matching `tests/fixtures/` module and register new modules in `tests/conftest.py` `pytest_plugins`. **The one exception: SigNoz-level fixtures in a suite's `conftest.py`.** A suite that needs its own SigNoz spun up with different envs (`create_signoz`/`create_migrator` with `env_overrides` + `cache_key` — e.g. basepath, metricreduction, querier_json_body) keeps that in its `conftest.py`; that is always okay.
- **Fixture only when there is a lifecycle; otherwise a plain function.** A fixture earns its indirection by owning setup/teardown (`yield` + cleanup — `insert_metrics` truncating on teardown) or by provisioning a resource (containers, SigNoz instances). A stateless action or lookup (`create_saved_view`, `find_saved_view_by_name`, wiping a resource list) is a plain importable function in the matching `tests/fixtures/` module, taking `signoz`/`token` as ordinary arguments — never wrap a plain callable in a fixture-factory just to inject `signoz`.
- **Fixtures own their cleanup.** When a test needs seeded state, put the seed + cleanup pair in a fixture (`yield`, then tear down) so tests in the same suite don't interfere — the pattern `insert_metrics` sets: yield a callable, truncate on teardown.
- **Fixture-factory over indirect parametrization.** A fixture that yields a callable (e.g. `insert_metrics(metrics)`) is clearer than `@pytest.mark.parametrize(..., indirect=True)` + `request.param` — the value is an explicit argument, not resolved by magic.
- **Skip at collection, not inside the test body.** Use `pytest.param(..., marks=pytest.mark.skip(reason="…"))` so a skipped case shows as SKIPPED-with-reason **and** short-circuits before its fixtures run (no environment spin-up for a test that won't execute).
- **Test config comes from explicit `--flags`, not the environment.** Wire configuration as pytest options declared in `tests/conftest.py` (`pytest_addoption` — e.g. `--sqlstore-provider`, `--clickhouse-version`); do **not** add `os.environ` fallbacks inside tests or fixtures.
- **snake_case parametrize ids.** `ids=["fill_gaps", "fill_zero"]`, not camelCase.
- **Name suite files with the two-digit prefix (`NN_*.py`).** `pyproject.toml` restricts collection to `[0-9][0-9]_*.py` (plus the bootstrap `setup.py` / `run.py`) — a file that doesn't match is silently never collected.
- **Always run pytest from `tests/`.** `--import-mode=importlib` is what allows same-basename files across suites (`querier/01_logs.py` vs `rawexportdata/01_logs.py`), but it disables pytest's implicit `sys.path` injection — `import fixtures` only resolves via `pythonpath = ["."]` from that rootdir.

View File

@@ -53,21 +53,6 @@ jobs:
with:
PRIMUS_REF: main
GO_VERSION: 1.24
semconv-generated:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: go-install
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: check-semconv-generated-files
run: go run ./scripts/semconv -check
build:
if: |
github.event_name == 'merge_group' ||

View File

@@ -58,6 +58,7 @@ jobs:
- querierai
- rawexportdata
- promqlconformance
- promapiconformance
- querierauthz
- role
- rootuser

View File

@@ -233,10 +233,6 @@ py-clean: ## Clear all pycache and pytest cache from tests directory recursively
##############################################################
# generate commands
##############################################################
.PHONY: semconv-generate
semconv-generate: ## Regenerate semantic-convention families for Go and TypeScript
@go run ./scripts/semconv
.PHONY: gen-mocks
gen-mocks:
@echo ">> Generating mocks"

View File

@@ -4212,21 +4212,6 @@ components:
- missingOptionalMetrics
- missingRequiredAttributes
type: object
InframonitoringtypesClusterFilter:
properties:
expression:
type: string
filterByNodeReadiness:
items:
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
nullable: true
type: array
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesClusterRecord:
properties:
clusterCPU:
@@ -4364,16 +4349,6 @@ components:
- containerCannotRun
- unknown
type: object
InframonitoringtypesContainerFilter:
properties:
expression:
type: string
filterByContainerStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesContainerStatus'
nullable: true
type: array
type: object
InframonitoringtypesContainerReady:
enum:
- ready
@@ -4473,16 +4448,6 @@ components:
- total
- endTimeBeforeRetention
type: object
InframonitoringtypesDaemonSetFilter:
properties:
expression:
type: string
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesDaemonSetRecord:
properties:
currentNodes:
@@ -4555,16 +4520,6 @@ components:
- total
- endTimeBeforeRetention
type: object
InframonitoringtypesDeploymentFilter:
properties:
expression:
type: string
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesDeploymentRecord:
properties:
availablePods:
@@ -4706,16 +4661,6 @@ components:
- total
- endTimeBeforeRetention
type: object
InframonitoringtypesJobFilter:
properties:
expression:
type: string
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesJobRecord:
properties:
activePods:
@@ -4839,16 +4784,6 @@ components:
- message
- documentationLink
type: object
InframonitoringtypesNamespaceFilter:
properties:
expression:
type: string
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesNamespaceRecord:
properties:
counts:
@@ -4930,21 +4865,6 @@ components:
- ready
- notReady
type: object
InframonitoringtypesNodeFilter:
properties:
expression:
type: string
filterByNodeReadiness:
items:
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
nullable: true
type: array
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesNodeRecord:
properties:
condition:
@@ -5061,16 +4981,6 @@ components:
- shutdown
- unexpectedAdmissionError
type: object
InframonitoringtypesPodFilter:
properties:
expression:
type: string
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesPodRecord:
properties:
meta:
@@ -5170,7 +5080,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesClusterFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5196,7 +5106,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesContainerFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5222,7 +5132,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesDaemonSetFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5248,7 +5158,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesDeploymentFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5300,7 +5210,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesJobFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5326,7 +5236,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesNamespaceFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5352,7 +5262,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesNodeFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5378,7 +5288,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesPodFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5404,7 +5314,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesStatefulSetFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5455,16 +5365,6 @@ components:
- list
- grouped_list
type: string
InframonitoringtypesStatefulSetFilter:
properties:
expression:
type: string
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesStatefulSetRecord:
properties:
currentPods:
@@ -7880,20 +7780,17 @@ components:
type: string
SavedviewtypesPostableSavedView:
properties:
data:
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
generateName:
type: boolean
name:
type: string
schemaVersion:
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
source:
$ref: '#/components/schemas/SavedviewtypesSource'
spec:
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
required:
- source
- schemaVersion
- spec
- data
type: object
SavedviewtypesSavedView:
properties:
@@ -7902,16 +7799,14 @@ components:
type: string
createdBy:
type: string
data:
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
id:
type: string
name:
type: string
schemaVersion:
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
source:
$ref: '#/components/schemas/SavedviewtypesSource'
spec:
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
updatedAt:
format: date-time
type: string
@@ -7919,6 +7814,14 @@ components:
type: string
required:
- id
type: object
SavedviewtypesSavedViewData:
properties:
schemaVersion:
type: string
spec:
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
required:
- schemaVersion
- spec
type: object
@@ -7933,7 +7836,6 @@ components:
queries:
items:
$ref: '#/components/schemas/Querybuildertypesv5QueryEnvelope'
minItems: 1
type: array
selectedFields:
items:
@@ -7943,11 +7845,9 @@ components:
- displayName
- panelType
- queries
- selectedFields
- display
type: object
SavedviewtypesSchemaVersion:
enum:
- v2
type: string
SavedviewtypesSource:
enum:
- traces
@@ -7957,16 +7857,13 @@ components:
type: string
SavedviewtypesUpdatableSavedView:
properties:
schemaVersion:
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
data:
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
source:
$ref: '#/components/schemas/SavedviewtypesSource'
spec:
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
required:
- source
- schemaVersion
- spec
- data
type: object
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
properties:
@@ -22975,12 +22872,6 @@ paths:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:

View File

@@ -5648,47 +5648,6 @@ export interface InframonitoringtypesChecksDTO {
type: InframonitoringtypesCheckTypeDTO;
}
export enum InframonitoringtypesNodeConditionDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
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 InframonitoringtypesClusterFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO[] | null;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesClusterRecordDTOCounts = {
/**
* @type integer
@@ -5964,6 +5923,21 @@ export interface InframonitoringtypesContainerCountsByStatusDTO {
waiting: number;
}
export enum InframonitoringtypesContainerReadyDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export type InframonitoringtypesContainerRecordDTOMetaAnyOf = {
[key: string]: string;
};
/**
* @nullable
*/
export type InframonitoringtypesContainerRecordDTOMeta =
InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
export enum InframonitoringtypesContainerStatusDTO {
running = 'running',
waiting = 'waiting',
@@ -5980,32 +5954,6 @@ export enum InframonitoringtypesContainerStatusDTO {
unknown = 'unknown',
no_data = 'no_data',
}
export interface InframonitoringtypesContainerFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByContainerStatus?: InframonitoringtypesContainerStatusDTO[] | null;
}
export enum InframonitoringtypesContainerReadyDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export type InframonitoringtypesContainerRecordDTOMetaAnyOf = {
[key: string]: string;
};
/**
* @nullable
*/
export type InframonitoringtypesContainerRecordDTOMeta =
InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
export interface InframonitoringtypesContainerRecordDTO {
containerCountsByReady: InframonitoringtypesContainerCountsByReadyDTO;
containerCountsByStatus: InframonitoringtypesContainerCountsByStatusDTO;
@@ -6077,17 +6025,6 @@ export interface InframonitoringtypesContainersDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesDaemonSetFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesDaemonSetRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6173,17 +6110,6 @@ export interface InframonitoringtypesDaemonSetsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesDeploymentFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesDeploymentRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6346,17 +6272,6 @@ export interface InframonitoringtypesHostsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesJobFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesJobRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6442,17 +6357,6 @@ export interface InframonitoringtypesJobsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesNamespaceFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesNamespaceRecordDTOCounts = {
/**
* @type integer
@@ -6529,21 +6433,11 @@ export interface InframonitoringtypesNamespacesDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesNodeFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO[] | null;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
export enum InframonitoringtypesNodeConditionDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export type InframonitoringtypesNodeRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6605,17 +6499,6 @@ export interface InframonitoringtypesNodesDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesPodFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6626,6 +6509,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
@@ -6702,7 +6606,7 @@ export interface InframonitoringtypesPostableClustersDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesClusterFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6729,7 +6633,7 @@ export interface InframonitoringtypesPostableContainersDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesContainerFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6756,7 +6660,7 @@ export interface InframonitoringtypesPostableDaemonSetsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesDaemonSetFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6783,7 +6687,7 @@ export interface InframonitoringtypesPostableDeploymentsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesDeploymentFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6837,7 +6741,7 @@ export interface InframonitoringtypesPostableJobsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesJobFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6864,7 +6768,7 @@ export interface InframonitoringtypesPostableNamespacesDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesNamespaceFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6891,7 +6795,7 @@ export interface InframonitoringtypesPostableNodesDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesNodeFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6918,7 +6822,7 @@ export interface InframonitoringtypesPostablePodsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesPodFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6939,24 +6843,13 @@ export interface InframonitoringtypesPostablePodsDTO {
start: number;
}
export interface InframonitoringtypesStatefulSetFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export interface InframonitoringtypesPostableStatefulSetsDTO {
/**
* @type integer
* @format int64
*/
end: number;
filter?: InframonitoringtypesStatefulSetFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -8991,17 +8884,8 @@ export enum SavedviewtypesPanelTypeDTO {
list = 'list',
trace = 'trace',
}
export enum SavedviewtypesSchemaVersionDTO {
v2 = 'v2',
}
export enum SavedviewtypesSourceDTO {
traces = 'traces',
logs = 'logs',
metrics = 'metrics',
meter = 'meter',
}
export interface SavedviewtypesSavedViewSpecDTO {
display?: SavedviewtypesDisplayDTO;
display: SavedviewtypesDisplayDTO;
/**
* @type string
*/
@@ -9014,10 +8898,25 @@ export interface SavedviewtypesSavedViewSpecDTO {
/**
* @type array
*/
selectedFields?: TelemetrytypesTelemetryFieldKeyDTO[];
selectedFields: TelemetrytypesTelemetryFieldKeyDTO[];
}
export interface SavedviewtypesSavedViewDataDTO {
/**
* @type string
*/
schemaVersion: string;
spec: SavedviewtypesSavedViewSpecDTO;
}
export enum SavedviewtypesSourceDTO {
traces = 'traces',
logs = 'logs',
metrics = 'metrics',
meter = 'meter',
}
export interface SavedviewtypesPostableSavedViewDTO {
data: SavedviewtypesSavedViewDataDTO;
/**
* @type boolean
*/
@@ -9026,9 +8925,7 @@ export interface SavedviewtypesPostableSavedViewDTO {
* @type string
*/
name?: string;
schemaVersion: SavedviewtypesSchemaVersionDTO;
source: SavedviewtypesSourceDTO;
spec: SavedviewtypesSavedViewSpecDTO;
}
export interface SavedviewtypesSavedViewDTO {
@@ -9041,6 +8938,7 @@ export interface SavedviewtypesSavedViewDTO {
* @type string
*/
createdBy?: string;
data?: SavedviewtypesSavedViewDataDTO;
/**
* @type string
*/
@@ -9049,9 +8947,7 @@ export interface SavedviewtypesSavedViewDTO {
* @type string
*/
name?: string;
schemaVersion: SavedviewtypesSchemaVersionDTO;
source?: SavedviewtypesSourceDTO;
spec: SavedviewtypesSavedViewSpecDTO;
/**
* @type string
* @format date-time
@@ -9064,9 +8960,8 @@ export interface SavedviewtypesSavedViewDTO {
}
export interface SavedviewtypesUpdatableSavedViewDTO {
schemaVersion: SavedviewtypesSchemaVersionDTO;
data: SavedviewtypesSavedViewDataDTO;
source: SavedviewtypesSourceDTO;
spec: SavedviewtypesSavedViewSpecDTO;
}
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {

View File

@@ -1,32 +0,0 @@
// Code generated by scripts/semconv. DO NOT EDIT.
export type SemconvFamily = {
readonly current: string;
readonly old: readonly string[];
readonly kind: 'attribute' | 'metric';
readonly contexts: readonly string[];
readonly signals: readonly string[];
readonly applyToMetrics: readonly string[];
readonly valueMap: Readonly<Record<string, string>>;
};
export const SEMCONV_FAMILIES: readonly SemconvFamily[] = [
{
current: 'db.system.name',
old: ['db.system'],
kind: 'attribute',
contexts: [],
signals: [],
applyToMetrics: [],
valueMap: {},
},
{
current: 'deployment.environment.name',
old: ['deployment.environment'],
kind: 'attribute',
contexts: [],
signals: [],
applyToMetrics: [],
valueMap: {},
},
] as const;

View File

@@ -51,7 +51,7 @@ func (provider *provider) addSavedViewRoutes(router *mux.Router) error {
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceSavedView.Scope(coretypes.VerbCreate)}),
},

View File

@@ -84,38 +84,20 @@ func buildClusterRecords(
return records
}
// getTopClusterGroupsAndMetadata concurrently fetches metadata + the ordering-metric
// ranking (plus the full-scope pod-status / node-readiness keysets when filtering,
// to intersect all).
func (m *module) getTopClusterGroupsAndMetadata(
ctx context.Context,
orgID valuer.UUID,
req *inframonitoringtypes.PostableClusters,
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, map[string]nodeConditionCounts, error) {
) ([]map[string]string, map[string]map[string]string, error) {
var (
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
statusCounts map[string]podStatusCounts
statusWarning *qbtypes.QueryWarnData
nodeConditionCounts map[string]nodeConditionCounts
filter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
filterByNodeReadiness []inframonitoringtypes.NodeCondition
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
)
orderByKey = req.OrderBy.Key.Name
// When filtering by pod status / node readiness, resolve the full-scope
// keyset(s) concurrently (pageGroups=nil spans all groups under the user
// filter) to intersect metadata + ranked groups below. Filters compose as AND.
if req.Filter != nil {
filter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
filterByNodeReadiness = req.Filter.FilterByNodeReadiness
}
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -124,37 +106,12 @@ func (m *module) getTopClusterGroupsAndMetadata(
return err
})
if len(filterByPodStatus) != 0 {
g.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if len(filterByNodeReadiness) != 0 {
g.Go(func() error {
var err error
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByNodeReadiness)
return err
})
}
if orderByKey == inframonitoringtypes.ClusterNameAttrKey {
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, nil, err
}
// Secondary filter: keep only status/readiness-matching groups. A missing
// metric yields an empty statusCounts, so this correctly empties the result
// (the caller also surfaces the warning). Filters compose as AND.
if len(filterByPodStatus) != 0 {
metadataMap = intersectMap(metadataMap, statusCounts)
}
if len(filterByNodeReadiness) != 0 {
metadataMap = intersectMap(metadataMap, nodeConditionCounts)
return nil, nil, err
}
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.ClusterNameAttrKey)
return pageGroups, metadataMap, statusCounts, statusWarning, nodeConditionCounts, nil
return pageGroups, metadataMap, nil
}
queryNamesForOrderBy := orderByToClustersQueryNames[orderByKey]
@@ -200,23 +157,10 @@ func (m *module) getTopClusterGroupsAndMetadata(
})
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, nil, err
return nil, nil, err
}
// Secondary filter: intersect ranked groups + metadata with the status/readiness
// keyset. A missing metric yields an empty keyset, correctly emptying the result
// (the caller also surfaces the warning). Filters compose as AND.
if len(filterByPodStatus) != 0 {
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
metadataMap = intersectMap(metadataMap, statusCounts)
}
if len(filterByNodeReadiness) != 0 {
allMetricGroups = intersectRankedGroups(allMetricGroups, nodeConditionCounts)
metadataMap = intersectMap(metadataMap, nodeConditionCounts)
}
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
return pageGroups, metadataMap, statusCounts, statusWarning, nodeConditionCounts, nil
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
}
func (m *module) getClustersTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableClusters) (map[string]map[string]string, error) {
@@ -226,9 +170,5 @@ func (m *module) getClustersTableMetadata(ctx context.Context, orgID valuer.UUID
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, clustersTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, clustersTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}

View File

@@ -139,34 +139,20 @@ func buildContainerRecords(
return records
}
// getTopContainerGroupsAndMetadata concurrently fetches metadata + the ordering-metric
// ranking (plus the full-scope container-status keyset when filtering, to intersect both).
func (m *module) getTopContainerGroupsAndMetadata(
ctx context.Context,
orgID valuer.UUID,
req *inframonitoringtypes.PostableContainers,
) ([]map[string]string, map[string]map[string]string, map[string]containerStatusCounts, *qbtypes.QueryWarnData, error) {
) ([]map[string]string, map[string]map[string]string, error) {
var (
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
statusCounts map[string]containerStatusCounts
statusWarning *qbtypes.QueryWarnData
filter *qbtypes.Filter
filterByContainerStatus []inframonitoringtypes.ContainerStatus
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
)
orderByKey = req.OrderBy.Key.Name
// When filtering by container status, resolve the full-scope status keyset
// concurrently (pageGroups=nil spans all groups under the user filter) so it
// can intersect metadata + ranked groups below.
if req.Filter != nil {
filter = &req.Filter.Filter
filterByContainerStatus = req.Filter.FilterByContainerStatus
}
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -175,26 +161,12 @@ func (m *module) getTopContainerGroupsAndMetadata(
return err
})
if len(filterByContainerStatus) != 0 {
g.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupContainerStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByContainerStatus)
return err
})
}
if orderByKey == inframonitoringtypes.ContainerNameAttrKey {
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, err
}
// Secondary filter: keep only status-matching groups. A missing metric
// yields an empty statusCounts, so this correctly empties the result
// (the caller also surfaces the warning).
if len(filterByContainerStatus) != 0 {
metadataMap = intersectMap(metadataMap, statusCounts)
return nil, nil, err
}
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.ContainerNameAttrKey)
return pageGroups, metadataMap, statusCounts, statusWarning, nil
return pageGroups, metadataMap, nil
}
queryNamesForOrderBy := orderByToContainersQueryNames[orderByKey]
@@ -240,19 +212,10 @@ func (m *module) getTopContainerGroupsAndMetadata(
})
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, err
return nil, nil, err
}
// Secondary filter: intersect ranked groups + metadata with the status keyset.
// A missing metric yields an empty statusCounts, correctly emptying the result
// (the caller also surfaces the warning).
if len(filterByContainerStatus) != 0 {
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
metadataMap = intersectMap(metadataMap, statusCounts)
}
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
return pageGroups, metadataMap, statusCounts, statusWarning, nil
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
}
func (m *module) getContainersTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableContainers) (map[string]map[string]string, error) {
@@ -262,11 +225,7 @@ func (m *module) getContainersTableMetadata(ctx context.Context, orgID valuer.UU
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, containersTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, containersTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}
// getPerGroupContainerStatusCountsWithReqMetricChecks gates
@@ -282,7 +241,6 @@ func (m *module) getPerGroupContainerStatusCountsWithReqMetricChecks(
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
pageGroups []map[string]string,
filterByContainerStatus []inframonitoringtypes.ContainerStatus,
) (map[string]containerStatusCounts, *qbtypes.QueryWarnData, error) {
present, err := m.getMetricsExistence(ctx, containerStatusMetricNamesList)
if err != nil {
@@ -308,28 +266,13 @@ func (m *module) getPerGroupContainerStatusCountsWithReqMetricChecks(
return map[string]containerStatusCounts{}, warning, nil
}
counts, err := m.getPerGroupContainerStatusCounts(ctx, orgID, start, end, filter, groupBy, pageGroups, filterByContainerStatus)
counts, err := m.getPerGroupContainerStatusCounts(ctx, orgID, start, end, filter, groupBy, pageGroups)
if err != nil {
return nil, nil, err
}
return counts, nil, nil
}
// applyContainerStatusFilter adds the display-status push-down (lower(display_status)
// IN (...)) to the outer count builder. valuer lowercases the wire value while
// display_status is kubectl-cased, so we compare lower() on both. No-op when the
// requested set is empty.
func applyContainerStatusFilter(cb *sqlbuilder.SelectBuilder, filterByContainerStatus []inframonitoringtypes.ContainerStatus) {
if len(filterByContainerStatus) == 0 {
return
}
vals := make([]string, len(filterByContainerStatus))
for i, c := range filterByContainerStatus {
vals[i] = c.StringValue()
}
cb.Where(cb.In("lower(display_status)", sqlbuilder.List(vals)))
}
// getPerGroupContainerStatusCounts computes per-group counts of distinct
// containers bucketed by their latest kubectl-style display status in window.
// Caller must ensure the required metrics exist
@@ -354,11 +297,8 @@ func (m *module) getPerGroupContainerStatusCounts(
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
pageGroups []map[string]string,
filterByContainerStatus []inframonitoringtypes.ContainerStatus,
) (map[string]containerStatusCounts, error) {
// Empty pageGroups means "span all under user filter", allowed only in
// full-scope mode (filtering by status). Otherwise it's an empty page.
if len(groupBy) == 0 || (len(pageGroups) == 0 && len(filterByContainerStatus) == 0) {
if len(pageGroups) == 0 || len(groupBy) == 0 {
return map[string]containerStatusCounts{}, nil
}
@@ -542,15 +482,11 @@ func (m *module) getPerGroupContainerStatusCounts(
countGroupBy = append(countGroupBy, col)
}
countSelectCols = append(countSelectCols, statusCountCols...)
// Outer count query. Built with sqlbuilder so the status push-down uses a
// proper IN (keep only containers whose display status is in the requested set).
countBuilder := sqlbuilder.NewSelectBuilder()
countBuilder.Select(countSelectCols...)
countBuilder.From("container_status")
applyContainerStatusFilter(countBuilder, filterByContainerStatus)
countBuilder.GroupBy(countGroupBy...)
countSQL, countArgs := countBuilder.BuildWithFlavor(sqlbuilder.ClickHouse)
countSQL := fmt.Sprintf(
"SELECT %s FROM container_status GROUP BY %s",
strings.Join(countSelectCols, ", "),
strings.Join(countGroupBy, ", "),
)
// Combine CTEs + outer. Arg order mirrors CTE declaration order.
cteFragments := []string{
@@ -563,7 +499,7 @@ func (m *module) getPerGroupContainerStatusCounts(
finalSQL := querybuilder.CombineCTEs(cteFragments) + countSQL
finalArgs := querybuilder.PrependArgs([][]any{
stateFpsArgs, containerStateArgs, reasonFpsArgs, reasonInnerArgs,
}, countArgs)
}, nil)
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
if err != nil {

View File

@@ -1,59 +0,0 @@
package implinframonitoring
import (
"strings"
"testing"
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
"github.com/huandu/go-sqlbuilder"
"github.com/stretchr/testify/assert"
)
func TestApplyContainerStatusFilter(t *testing.T) {
tests := []struct {
name string
statuses []inframonitoringtypes.ContainerStatus
wantWhere bool
wantArgs []any
}{
{
name: "empty set yields no clause",
statuses: nil,
wantWhere: false,
wantArgs: nil,
},
{
name: "single status pushes lowercased arg via IN",
statuses: []inframonitoringtypes.ContainerStatus{inframonitoringtypes.ContainerStatusRunning},
wantWhere: true,
wantArgs: []any{"running"},
},
{
name: "multiple statuses push lowercased args via IN",
statuses: []inframonitoringtypes.ContainerStatus{
inframonitoringtypes.ContainerStatusRunning,
inframonitoringtypes.ContainerStatusCrashLoopBackOff,
},
wantWhere: true,
wantArgs: []any{"running", "crashloopbackoff"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cb := sqlbuilder.NewSelectBuilder()
cb.Select("pod_uid")
cb.From("container_status")
applyContainerStatusFilter(cb, tt.statuses)
sql, args := cb.BuildWithFlavor(sqlbuilder.ClickHouse)
hasWhere := strings.Contains(sql, "lower(display_status) IN (")
assert.Equal(t, tt.wantWhere, hasWhere)
if len(tt.wantArgs) == 0 {
assert.Empty(t, args)
} else {
assert.Equal(t, tt.wantArgs, args)
}
})
}
}

View File

@@ -90,34 +90,20 @@ func buildDaemonSetRecords(
return records
}
// getTopDaemonSetGroupsAndMetadata concurrently fetches metadata + the ordering-metric
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
func (m *module) getTopDaemonSetGroupsAndMetadata(
ctx context.Context,
orgID valuer.UUID,
req *inframonitoringtypes.PostableDaemonSets,
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
) ([]map[string]string, map[string]map[string]string, error) {
var (
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
statusCounts map[string]podStatusCounts
statusWarning *qbtypes.QueryWarnData
filter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
)
orderByKey = req.OrderBy.Key.Name
// When filtering by pod status, resolve the full-scope status keyset
// concurrently (pageGroups=nil spans all groups under the user filter) so it
// can intersect metadata + ranked groups below.
if req.Filter != nil {
filter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -126,26 +112,12 @@ func (m *module) getTopDaemonSetGroupsAndMetadata(
return err
})
if len(filterByPodStatus) != 0 {
g.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if orderByKey == inframonitoringtypes.DaemonSetNameAttrKey {
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, err
}
// Secondary filter: keep only status-matching groups. A missing metric
// yields an empty statusCounts, so this correctly empties the result
// (the caller also surfaces the warning).
if len(filterByPodStatus) != 0 {
metadataMap = intersectMap(metadataMap, statusCounts)
return nil, nil, err
}
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DaemonSetNameAttrKey)
return pageGroups, metadataMap, statusCounts, statusWarning, nil
return pageGroups, metadataMap, nil
}
queryNamesForOrderBy := orderByToDaemonSetsQueryNames[orderByKey]
@@ -191,19 +163,10 @@ func (m *module) getTopDaemonSetGroupsAndMetadata(
})
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, err
return nil, nil, err
}
// Secondary filter: intersect ranked groups + metadata with the status keyset.
// A missing metric yields an empty statusCounts, correctly emptying the result
// (the caller also surfaces the warning).
if len(filterByPodStatus) != 0 {
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
metadataMap = intersectMap(metadataMap, statusCounts)
}
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
return pageGroups, metadataMap, statusCounts, statusWarning, nil
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
}
func (m *module) getDaemonSetsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableDaemonSets) (map[string]map[string]string, error) {
@@ -213,9 +176,5 @@ func (m *module) getDaemonSetsTableMetadata(ctx context.Context, orgID valuer.UU
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, daemonSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, daemonSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}

View File

@@ -82,34 +82,20 @@ func buildDeploymentRecords(
return records
}
// getTopDeploymentGroupsAndMetadata concurrently fetches metadata + the ordering-metric
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
func (m *module) getTopDeploymentGroupsAndMetadata(
ctx context.Context,
orgID valuer.UUID,
req *inframonitoringtypes.PostableDeployments,
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
) ([]map[string]string, map[string]map[string]string, error) {
var (
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
statusCounts map[string]podStatusCounts
statusWarning *qbtypes.QueryWarnData
filter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
)
orderByKey = req.OrderBy.Key.Name
// When filtering by pod status, resolve the full-scope status keyset
// concurrently (pageGroups=nil spans all groups under the user filter) so it
// can intersect metadata + ranked groups below.
if req.Filter != nil {
filter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -118,26 +104,12 @@ func (m *module) getTopDeploymentGroupsAndMetadata(
return err
})
if len(filterByPodStatus) != 0 {
g.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if orderByKey == inframonitoringtypes.DeploymentNameAttrKey {
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, err
}
// Secondary filter: keep only status-matching groups. A missing metric
// yields an empty statusCounts, so this correctly empties the result
// (the caller also surfaces the warning).
if len(filterByPodStatus) != 0 {
metadataMap = intersectMap(metadataMap, statusCounts)
return nil, nil, err
}
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DeploymentNameAttrKey)
return pageGroups, metadataMap, statusCounts, statusWarning, nil
return pageGroups, metadataMap, nil
}
queryNamesForOrderBy := orderByToDeploymentsQueryNames[orderByKey]
@@ -183,19 +155,10 @@ func (m *module) getTopDeploymentGroupsAndMetadata(
})
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, err
return nil, nil, err
}
// Secondary filter: intersect ranked groups + metadata with the status keyset.
// A missing metric yields an empty statusCounts, correctly emptying the result
// (the caller also surfaces the warning).
if len(filterByPodStatus) != 0 {
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
metadataMap = intersectMap(metadataMap, statusCounts)
}
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
return pageGroups, metadataMap, statusCounts, statusWarning, nil
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
}
func (m *module) getDeploymentsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableDeployments) (map[string]map[string]string, error) {
@@ -205,9 +168,5 @@ func (m *module) getDeploymentsTableMetadata(ctx context.Context, orgID valuer.U
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, deploymentsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, deploymentsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}

View File

@@ -63,33 +63,6 @@ func compositeKeyFromLabels(labels map[string]string, groupBy []qbtypes.GroupByK
return compositeKeyFromList(parts)
}
// intersectMap returns the entries of m whose key is present in keep (a new
// map). keep's value type is irrelevant — only its keys are read — so a
// per-group counts map (already filtered by the SQL push-down) can be passed
// directly. Used to trim metadataMap to the status-matching groups.
func intersectMap[V any, K any](m map[string]V, keep map[string]K) map[string]V {
out := make(map[string]V, len(m))
for k, v := range m {
if _, ok := keep[k]; ok {
out[k] = v
}
}
return out
}
// intersectRankedGroups returns the ranked groups whose compositeKey is present
// in keep, preserving order. Keeps status-unmatched groups out of the ranked
// page slots.
func intersectRankedGroups[K any](groups []rankedGroup, keep map[string]K) []rankedGroup {
out := make([]rankedGroup, 0, len(groups))
for _, g := range groups {
if _, ok := keep[g.compositeKey]; ok {
out = append(out, g)
}
}
return out
}
// parseAndSortGroups extracts group label maps from a ScalarData response and
// sorts them by the ranking query's aggregation value.
func parseAndSortGroups(
@@ -877,10 +850,8 @@ func (m *module) getPerGroupDistinctCounts(
valueExpr = fmt.Sprintf("(%s)", strings.Join(parts, ", "))
}
// Prefix the alias so it never collides with a groupBy col alias
// (e.g. clusters grouped by k8s.node.name, which is also counted).
selectCols = append(selectCols,
fmt.Sprintf("uniqExactIf(%s, %s != '') AS %s", valueExpr, extract, quoteIdentifier(fmt.Sprintf("__count_%s", attr))),
fmt.Sprintf("uniqExactIf(%s, %s != '') AS %s", valueExpr, extract, quoteIdentifier(attr)),
)
}
sb.Select(selectCols...)

View File

@@ -5,7 +5,6 @@ import (
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
)
func groupByKey(name string) qbtypes.GroupByKey {
@@ -89,7 +88,10 @@ func TestIsKeyInGroupByAttrs(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isKeyInGroupByAttrs(tt.groupByAttrs, tt.key)
assert.Equal(t, tt.expectedFound, got)
if got != tt.expectedFound {
t.Errorf("isKeyInGroupByAttrs(%v, %q) = %v, want %v",
tt.groupByAttrs, tt.key, got, tt.expectedFound)
}
})
}
}
@@ -154,7 +156,10 @@ func TestMergeFilterExpressions(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := mergeFilterExpressions(tt.queryFilterExpr, tt.reqFilterExpr)
assert.Equal(t, tt.expected, got)
if got != tt.expected {
t.Errorf("mergeFilterExpressions(%q, %q) = %q, want %q",
tt.queryFilterExpr, tt.reqFilterExpr, got, tt.expected)
}
})
}
}
@@ -200,7 +205,10 @@ func TestCompositeKeyFromList(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := compositeKeyFromList(tt.parts)
assert.Equal(t, tt.expected, got)
if got != tt.expected {
t.Errorf("compositeKeyFromList(%v) = %q, want %q",
tt.parts, got, tt.expected)
}
})
}
}
@@ -368,81 +376,10 @@ func TestCompositeKeyFromLabels(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := compositeKeyFromLabels(tt.labels, tt.groupBy)
assert.Equal(t, tt.expected, got)
})
}
}
func TestIntersectMap(t *testing.T) {
tests := []struct {
name string
m map[string]int
keep map[string]podStatusCounts
expected map[string]int
}{
{
name: "keep subset",
m: map[string]int{"a": 1, "b": 2, "c": 3},
keep: map[string]podStatusCounts{"a": {}, "c": {}},
expected: map[string]int{"a": 1, "c": 3},
},
{
name: "empty keep drops everything",
m: map[string]int{"a": 1, "b": 2},
keep: map[string]podStatusCounts{},
expected: map[string]int{},
},
{
name: "keep key absent from m is ignored",
m: map[string]int{"a": 1},
keep: map[string]podStatusCounts{"a": {}, "z": {}},
expected: map[string]int{"a": 1},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := intersectMap(tt.m, tt.keep)
assert.Equal(t, tt.expected, got)
})
}
}
func TestIntersectRankedGroups(t *testing.T) {
groups := []rankedGroup{
{compositeKey: "a", value: 3},
{compositeKey: "b", value: 2},
{compositeKey: "c", value: 1},
}
tests := []struct {
name string
groups []rankedGroup
keep map[string]podStatusCounts
expected []string // compositeKeys in order
}{
{
name: "preserves order, drops non-matching",
groups: groups,
keep: map[string]podStatusCounts{"a": {}, "c": {}},
expected: []string{"a", "c"},
},
{
name: "empty keep drops all",
groups: groups,
keep: map[string]podStatusCounts{},
expected: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := intersectRankedGroups(tt.groups, tt.keep)
gotKeys := make([]string, 0, len(got))
for _, g := range got {
gotKeys = append(gotKeys, g.compositeKey)
if got != tt.expected {
t.Errorf("compositeKeyFromLabels(%v, %v) = %q, want %q",
tt.labels, tt.groupBy, got, tt.expected)
}
assert.Equal(t, tt.expected, gotKeys)
})
}
}

View File

@@ -90,34 +90,20 @@ func buildJobRecords(
return records
}
// getTopJobGroupsAndMetadata concurrently fetches metadata + the ordering-metric
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
func (m *module) getTopJobGroupsAndMetadata(
ctx context.Context,
orgID valuer.UUID,
req *inframonitoringtypes.PostableJobs,
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
) ([]map[string]string, map[string]map[string]string, error) {
var (
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
statusCounts map[string]podStatusCounts
statusWarning *qbtypes.QueryWarnData
filter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
)
orderByKey = req.OrderBy.Key.Name
// When filtering by pod status, resolve the full-scope status keyset
// concurrently (pageGroups=nil spans all groups under the user filter) so it
// can intersect metadata + ranked groups below.
if req.Filter != nil {
filter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -126,26 +112,12 @@ func (m *module) getTopJobGroupsAndMetadata(
return err
})
if len(filterByPodStatus) != 0 {
g.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if orderByKey == inframonitoringtypes.JobNameAttrKey {
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, err
}
// Secondary filter: keep only status-matching groups. A missing metric
// yields an empty statusCounts, so this correctly empties the result
// (the caller also surfaces the warning).
if len(filterByPodStatus) != 0 {
metadataMap = intersectMap(metadataMap, statusCounts)
return nil, nil, err
}
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.JobNameAttrKey)
return pageGroups, metadataMap, statusCounts, statusWarning, nil
return pageGroups, metadataMap, nil
}
queryNamesForOrderBy := orderByToJobsQueryNames[orderByKey]
@@ -191,19 +163,10 @@ func (m *module) getTopJobGroupsAndMetadata(
})
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, err
return nil, nil, err
}
// Secondary filter: intersect ranked groups + metadata with the status keyset.
// A missing metric yields an empty statusCounts, correctly emptying the result
// (the caller also surfaces the warning).
if len(filterByPodStatus) != 0 {
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
metadataMap = intersectMap(metadataMap, statusCounts)
}
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
return pageGroups, metadataMap, statusCounts, statusWarning, nil
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
}
func (m *module) getJobsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableJobs) (map[string]map[string]string, error) {
@@ -213,9 +176,5 @@ func (m *module) getJobsTableMetadata(ctx context.Context, orgID valuer.UUID, re
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, jobsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, jobsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}

View File

@@ -286,36 +286,11 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
return resp, nil
}
var (
filterExpr string
podFilter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
queryResp *qbtypes.QueryRangeResponse
restartCounts map[string]int64
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
podFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
// getTopPodGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
// status when filtering) concurrently, intersecting metadata/ranked groups
// against the status keyset. It returns the keyset + its warning.
pageGroups, metadataMap, statusCounts, statusWarning, err := m.getTopPodGroupsAndMetadata(ctx, orgID, req)
pageGroups, metadataMap, err := m.getTopPodGroupsAndMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
// Required metric missing while filtering: surface the warning + empty result.
if len(filterByPodStatus) != 0 && statusWarning != nil {
resp.Warning = statusWarning
resp.Records = []inframonitoringtypes.PodRecord{}
resp.Total = 0
return resp, nil
}
resp.Total = len(metadataMap)
if len(pageGroups) == 0 {
@@ -323,8 +298,20 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newPodsTableListQuery())
var (
queryResp *qbtypes.QueryRangeResponse
statusCounts map[string]podStatusCounts
statusWarning *qbtypes.QueryWarnData
restartCounts map[string]int64
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -334,18 +321,14 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
})
g.Go(func() error {
var err error
restartCounts, err = m.getPerGroupPodRestartCounts(gCtx, orgID, req.Start, req.End, podFilter, req.GroupBy, pageGroups)
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
restartCounts, err = m.getPerGroupPodRestartCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
// When filtering, statusCounts already holds the full-scope map (a superset
// of the page); otherwise compute it page-scoped here.
if len(filterByPodStatus) == 0 {
g.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, podFilter, req.GroupBy, pageGroups, nil)
return err
})
}
if err := g.Wait(); err != nil {
return nil, err
@@ -396,37 +379,11 @@ func (m *module) ListContainers(ctx context.Context, orgID valuer.UUID, req *inf
return resp, nil
}
var (
filterExpr string
containerFilter *qbtypes.Filter
filterByContainerStatus []inframonitoringtypes.ContainerStatus
queryResp *qbtypes.QueryRangeResponse
restartCounts map[string]int64
readyCounts map[string]containerReadyCounts
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
containerFilter = &req.Filter.Filter
filterByContainerStatus = req.Filter.FilterByContainerStatus
}
// getTopContainerGroupsAndMetadata fetches metadata + ranking (+ full-scope
// container status when filtering) concurrently, intersecting metadata/ranked
// groups against the status keyset. It returns the keyset + its warning.
pageGroups, metadataMap, statusCounts, statusWarning, err := m.getTopContainerGroupsAndMetadata(ctx, orgID, req)
pageGroups, metadataMap, err := m.getTopContainerGroupsAndMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
// Required metric missing while filtering: surface the warning + empty result.
if len(filterByContainerStatus) != 0 && statusWarning != nil {
resp.Warning = statusWarning
resp.Records = []inframonitoringtypes.ContainerRecord{}
resp.Total = 0
return resp, nil
}
resp.Total = len(metadataMap)
if len(pageGroups) == 0 {
@@ -434,8 +391,21 @@ func (m *module) ListContainers(ctx context.Context, orgID valuer.UUID, req *inf
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newContainersTableListQuery())
var (
queryResp *qbtypes.QueryRangeResponse
statusCounts map[string]containerStatusCounts
statusWarning *qbtypes.QueryWarnData
restartCounts map[string]int64
readyCounts map[string]containerReadyCounts
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -445,23 +415,19 @@ func (m *module) ListContainers(ctx context.Context, orgID valuer.UUID, req *inf
})
g.Go(func() error {
var err error
restartCounts, err = m.getPerGroupContainerRestartCounts(gCtx, orgID, req.Start, req.End, containerFilter, req.GroupBy, pageGroups)
statusCounts, statusWarning, err = m.getPerGroupContainerStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
readyCounts, err = m.getPerGroupContainerReadyCounts(gCtx, orgID, req.Start, req.End, containerFilter, req.GroupBy, pageGroups)
restartCounts, err = m.getPerGroupContainerRestartCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
readyCounts, err = m.getPerGroupContainerReadyCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
// When filtering, statusCounts already holds the full-scope map (a superset
// of the page); otherwise compute it page-scoped here.
if len(filterByContainerStatus) == 0 {
g.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupContainerStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, containerFilter, req.GroupBy, pageGroups, nil)
return err
})
}
if err := g.Wait(); err != nil {
return nil, err
@@ -512,37 +478,11 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
return resp, nil
}
var (
filterExpr string
nodeFilter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
filterByNodeReadiness []inframonitoringtypes.NodeCondition
queryResp *qbtypes.QueryRangeResponse
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
nodeFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
filterByNodeReadiness = req.Filter.FilterByNodeReadiness
}
// getTopNodeGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
// status / node readiness when filtering) concurrently, intersecting
// metadata/ranked groups against the keysets. It returns the keysets + warning.
pageGroups, metadataMap, podStatusCounts, podStatusWarning, nodeConditionCounts, err := m.getTopNodeGroupsAndMetadata(ctx, orgID, req)
pageGroups, metadataMap, err := m.getTopNodeGroupsAndMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
// Required metric missing while filtering: surface the warning + empty result.
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
resp.Warning = podStatusWarning
resp.Records = []inframonitoringtypes.NodeRecord{}
resp.Total = 0
return resp, nil
}
resp.Total = len(metadataMap)
if len(pageGroups) == 0 {
@@ -550,8 +490,20 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newNodesTableListQuery())
var (
queryResp *qbtypes.QueryRangeResponse
nodeConditionCounts map[string]nodeConditionCounts
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -559,24 +511,16 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
return err
})
// When filtering by readiness, nodeConditionCounts already holds the full-scope
// map (a superset of the page); otherwise compute it page-scoped here.
if len(filterByNodeReadiness) == 0 {
g.Go(func() error {
var err error
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, nodeFilter, req.GroupBy, pageGroups, nil)
return err
})
}
// When filtering by pod status, podStatusCounts already holds the full-scope
// map; otherwise compute it page-scoped here.
if len(filterByPodStatus) == 0 {
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, nodeFilter, req.GroupBy, pageGroups, nil)
return err
})
}
g.Go(func() error {
var err error
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
if err := g.Wait(); err != nil {
return nil, err
@@ -627,36 +571,11 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
return resp, nil
}
var (
filterExpr string
namespaceFilter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
queryResp *qbtypes.QueryRangeResponse
resourceCounts map[string]map[string]int64
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
namespaceFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
// getTopNamespaceGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
// status when filtering) concurrently, intersecting metadata/ranked groups
// against the status keyset. It returns the keyset + its warning.
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopNamespaceGroupsAndMetadata(ctx, orgID, req)
pageGroups, metadataMap, err := m.getTopNamespaceGroupsAndMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
// Required metric missing while filtering: surface the warning + empty result.
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
resp.Warning = podStatusWarning
resp.Records = []inframonitoringtypes.NamespaceRecord{}
resp.Total = 0
return resp, nil
}
resp.Total = len(metadataMap)
if len(pageGroups) == 0 {
@@ -664,8 +583,20 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newNamespacesTableListQuery())
var (
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
resourceCounts map[string]map[string]int64
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -675,18 +606,14 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
})
g.Go(func() error {
var err error
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, namespaceFilter, req.GroupBy, pageGroups, namespaceCountAttrKeys, namespacesMetricNamesListForCounts)
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups, namespaceCountAttrKeys, namespacesMetricNamesListForCounts)
return err
})
// When filtering, podStatusCounts already holds the full-scope map (a superset
// of the page); otherwise compute it page-scoped here.
if len(filterByPodStatus) == 0 {
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, namespaceFilter, req.GroupBy, pageGroups, nil)
return err
})
}
if err := g.Wait(); err != nil {
return nil, err
@@ -736,39 +663,11 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
return resp, nil
}
var (
filterExpr string
clusterFilter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
filterByNodeReadiness []inframonitoringtypes.NodeCondition
queryResp *qbtypes.QueryRangeResponse
nodeConditionCountsMap map[string]nodeConditionCounts
resourceCounts map[string]map[string]int64
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
clusterFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
filterByNodeReadiness = req.Filter.FilterByNodeReadiness
}
// getTopClusterGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
// status / node readiness when filtering) concurrently, intersecting
// metadata/ranked groups against the keysets. It returns the keysets + warning.
pageGroups, metadataMap, podStatusCounts, podStatusWarning, nodeConditionCountsMap, err := m.getTopClusterGroupsAndMetadata(ctx, orgID, req)
pageGroups, metadataMap, err := m.getTopClusterGroupsAndMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
// Required metric missing while filtering: surface the warning + empty result.
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
resp.Warning = podStatusWarning
resp.Records = []inframonitoringtypes.ClusterRecord{}
resp.Total = 0
return resp, nil
}
resp.Total = len(metadataMap)
if len(pageGroups) == 0 {
@@ -776,8 +675,23 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newClustersTableListQuery())
// With default groupBy [k8s.cluster.name], counts are bucketed per cluster;
// with a custom groupBy, they aggregate across clusters in that group.
var (
queryResp *qbtypes.QueryRangeResponse
nodeConditionCountsMap map[string]nodeConditionCounts
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
resourceCounts map[string]map[string]int64
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -785,29 +699,21 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
return err
})
// When filtering by readiness, nodeConditionCountsMap already holds the
// full-scope map (a superset of the page); otherwise compute it page-scoped here.
if len(filterByNodeReadiness) == 0 {
g.Go(func() error {
var err error
nodeConditionCountsMap, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, clusterFilter, req.GroupBy, pageGroups, nil)
return err
})
}
g.Go(func() error {
var err error
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, clusterFilter, req.GroupBy, pageGroups, clusterCountAttrKeys, clusterMetricNamesListForCounts)
nodeConditionCountsMap, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups, clusterCountAttrKeys, clusterMetricNamesListForCounts)
return err
})
// When filtering by pod status, podStatusCounts already holds the full-scope
// map; otherwise compute it page-scoped here.
if len(filterByPodStatus) == 0 {
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, clusterFilter, req.GroupBy, pageGroups, nil)
return err
})
}
if err := g.Wait(); err != nil {
return nil, err
@@ -921,7 +827,7 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
// Bake the deployments base filter into req.Filter so all downstream helpers pick it up.
if req.Filter == nil {
req.Filter = &inframonitoringtypes.DeploymentFilter{}
req.Filter = &qbtypes.Filter{}
}
req.Filter.Expression = mergeFilterExpressions(deploymentsBaseFilterExpr, req.Filter.Expression)
@@ -936,35 +842,11 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
return resp, nil
}
var (
filterExpr string
deploymentFilter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
queryResp *qbtypes.QueryRangeResponse
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
deploymentFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
// getTopDeploymentGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
// status when filtering) concurrently, intersecting metadata/ranked groups
// against the status keyset. It returns the keyset + its warning.
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopDeploymentGroupsAndMetadata(ctx, orgID, req)
pageGroups, metadataMap, err := m.getTopDeploymentGroupsAndMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
// Required metric missing while filtering: surface the warning + empty result.
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
resp.Warning = podStatusWarning
resp.Records = []inframonitoringtypes.DeploymentRecord{}
resp.Total = 0
return resp, nil
}
resp.Total = len(metadataMap)
if len(pageGroups) == 0 {
@@ -972,8 +854,19 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newDeploymentsTableListQuery())
var (
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -981,15 +874,11 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
return err
})
// When filtering, podStatusCounts already holds the full-scope map (a superset
// of the page); otherwise compute it page-scoped here.
if len(filterByPodStatus) == 0 {
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, deploymentFilter, req.GroupBy, pageGroups, nil)
return err
})
}
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
if err := g.Wait(); err != nil {
return nil, err
@@ -1030,7 +919,7 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
// Bake the workload base filter into req.Filter so all downstream helpers pick it up.
if req.Filter == nil {
req.Filter = &inframonitoringtypes.StatefulSetFilter{}
req.Filter = &qbtypes.Filter{}
}
req.Filter.Expression = mergeFilterExpressions(statefulSetsBaseFilterExpr, req.Filter.Expression)
@@ -1045,35 +934,11 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
return resp, nil
}
var (
filterExpr string
statefulSetFilter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
queryResp *qbtypes.QueryRangeResponse
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
statefulSetFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
// getTopStatefulSetGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
// status when filtering) concurrently, intersecting metadata/ranked groups
// against the status keyset. It returns the keyset + its warning.
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopStatefulSetGroupsAndMetadata(ctx, orgID, req)
pageGroups, metadataMap, err := m.getTopStatefulSetGroupsAndMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
// Required metric missing while filtering: surface the warning + empty result.
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
resp.Warning = podStatusWarning
resp.Records = []inframonitoringtypes.StatefulSetRecord{}
resp.Total = 0
return resp, nil
}
resp.Total = len(metadataMap)
if len(pageGroups) == 0 {
@@ -1081,8 +946,21 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newStatefulSetsTableListQuery())
// Pods owned by a StatefulSet carry k8s.statefulset.name as a resource attribute,
// so default-groupBy gives per-statefulset status counts automatically.
var (
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -1090,15 +968,11 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
return err
})
// When filtering, podStatusCounts already holds the full-scope map (a superset
// of the page); otherwise compute it page-scoped here.
if len(filterByPodStatus) == 0 {
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, statefulSetFilter, req.GroupBy, pageGroups, nil)
return err
})
}
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
if err := g.Wait(); err != nil {
return nil, err
@@ -1139,7 +1013,7 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
// Bake the jobs base filter into req.Filter so all downstream helpers pick it up.
if req.Filter == nil {
req.Filter = &inframonitoringtypes.JobFilter{}
req.Filter = &qbtypes.Filter{}
}
req.Filter.Expression = mergeFilterExpressions(jobsBaseFilterExpr, req.Filter.Expression)
@@ -1154,35 +1028,11 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
return resp, nil
}
var (
filterExpr string
jobFilter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
queryResp *qbtypes.QueryRangeResponse
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
jobFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
// getTopJobGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
// status when filtering) concurrently, intersecting metadata/ranked groups
// against the status keyset. It returns the keyset + its warning.
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopJobGroupsAndMetadata(ctx, orgID, req)
pageGroups, metadataMap, err := m.getTopJobGroupsAndMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
// Required metric missing while filtering: surface the warning + empty result.
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
resp.Warning = podStatusWarning
resp.Records = []inframonitoringtypes.JobRecord{}
resp.Total = 0
return resp, nil
}
resp.Total = len(metadataMap)
if len(pageGroups) == 0 {
@@ -1190,8 +1040,21 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newJobsTableListQuery())
// Pods owned by a Job carry k8s.job.name as a resource attribute, so default-groupBy
// gives per-job status counts automatically.
var (
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -1199,15 +1062,11 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
return err
})
// When filtering, podStatusCounts already holds the full-scope map (a superset
// of the page); otherwise compute it page-scoped here.
if len(filterByPodStatus) == 0 {
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, jobFilter, req.GroupBy, pageGroups, nil)
return err
})
}
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
if err := g.Wait(); err != nil {
return nil, err
@@ -1248,7 +1107,7 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
// Bake the workload base filter into req.Filter so all downstream helpers pick it up.
if req.Filter == nil {
req.Filter = &inframonitoringtypes.DaemonSetFilter{}
req.Filter = &qbtypes.Filter{}
}
req.Filter.Expression = mergeFilterExpressions(daemonSetsBaseFilterExpr, req.Filter.Expression)
@@ -1263,35 +1122,11 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
return resp, nil
}
var (
filterExpr string
daemonSetFilter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
queryResp *qbtypes.QueryRangeResponse
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
daemonSetFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
// getTopDaemonSetGroupsAndMetadata fetches metadata + ranking (+ full-scope pod
// status when filtering) concurrently, intersecting metadata/ranked groups
// against the status keyset. It returns the keyset + its warning.
pageGroups, metadataMap, podStatusCounts, podStatusWarning, err := m.getTopDaemonSetGroupsAndMetadata(ctx, orgID, req)
pageGroups, metadataMap, err := m.getTopDaemonSetGroupsAndMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
// Required metric missing while filtering: surface the warning + empty result.
if len(filterByPodStatus) != 0 && podStatusWarning != nil {
resp.Warning = podStatusWarning
resp.Records = []inframonitoringtypes.DaemonSetRecord{}
resp.Total = 0
return resp, nil
}
resp.Total = len(metadataMap)
if len(pageGroups) == 0 {
@@ -1299,8 +1134,21 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newDaemonSetsTableListQuery())
// Pods owned by a DaemonSet carry k8s.daemonset.name as a resource attribute,
// so default-groupBy gives per-daemonset status counts automatically.
var (
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -1308,15 +1156,11 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
return err
})
// When filtering, podStatusCounts already holds the full-scope map (a superset
// of the page); otherwise compute it page-scoped here.
if len(filterByPodStatus) == 0 {
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, daemonSetFilter, req.GroupBy, pageGroups, nil)
return err
})
}
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
if err := g.Wait(); err != nil {
return nil, err

View File

@@ -65,34 +65,20 @@ func buildNamespaceRecords(
return records
}
// getTopNamespaceGroupsAndMetadata concurrently fetches metadata + the ordering-metric
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
func (m *module) getTopNamespaceGroupsAndMetadata(
ctx context.Context,
orgID valuer.UUID,
req *inframonitoringtypes.PostableNamespaces,
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
) ([]map[string]string, map[string]map[string]string, error) {
var (
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
statusCounts map[string]podStatusCounts
statusWarning *qbtypes.QueryWarnData
filter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
)
orderByKey = req.OrderBy.Key.Name
// When filtering by pod status, resolve the full-scope status keyset
// concurrently (pageGroups=nil spans all groups under the user filter) so it
// can intersect metadata + ranked groups below.
if req.Filter != nil {
filter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -101,26 +87,12 @@ func (m *module) getTopNamespaceGroupsAndMetadata(
return err
})
if len(filterByPodStatus) != 0 {
g.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if orderByKey == inframonitoringtypes.NamespaceNameAttrKey {
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, err
}
// Secondary filter: keep only status-matching groups. A missing metric
// yields an empty statusCounts, so this correctly empties the result
// (the caller also surfaces the warning).
if len(filterByPodStatus) != 0 {
metadataMap = intersectMap(metadataMap, statusCounts)
return nil, nil, err
}
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NamespaceNameAttrKey)
return pageGroups, metadataMap, statusCounts, statusWarning, nil
return pageGroups, metadataMap, nil
}
queryNamesForOrderBy := orderByToNamespacesQueryNames[orderByKey]
@@ -166,19 +138,10 @@ func (m *module) getTopNamespaceGroupsAndMetadata(
})
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, err
return nil, nil, err
}
// Secondary filter: intersect ranked groups + metadata with the status keyset.
// A missing metric yields an empty statusCounts, correctly emptying the result
// (the caller also surfaces the warning).
if len(filterByPodStatus) != 0 {
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
metadataMap = intersectMap(metadataMap, statusCounts)
}
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
return pageGroups, metadataMap, statusCounts, statusWarning, nil
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
}
func (m *module) getNamespacesTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableNamespaces) (map[string]map[string]string, error) {
@@ -188,9 +151,5 @@ func (m *module) getNamespacesTableMetadata(ctx context.Context, orgID valuer.UU
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, namespacesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, namespacesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"slices"
"strings"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetryschema/metricstelemetryschema"
@@ -91,38 +92,20 @@ func buildNodeRecords(
return records
}
// getTopNodeGroupsAndMetadata concurrently fetches metadata + the ordering-metric
// ranking (plus the full-scope pod-status / node-readiness keysets when filtering,
// to intersect all).
func (m *module) getTopNodeGroupsAndMetadata(
ctx context.Context,
orgID valuer.UUID,
req *inframonitoringtypes.PostableNodes,
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, map[string]nodeConditionCounts, error) {
) ([]map[string]string, map[string]map[string]string, error) {
var (
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
statusCounts map[string]podStatusCounts
statusWarning *qbtypes.QueryWarnData
nodeConditionCounts map[string]nodeConditionCounts
filter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
filterByNodeReadiness []inframonitoringtypes.NodeCondition
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
)
orderByKey = req.OrderBy.Key.Name
// When filtering by pod status / node readiness, resolve the full-scope
// keyset(s) concurrently (pageGroups=nil spans all groups under the user
// filter) to intersect metadata + ranked groups below. Filters compose as AND.
if req.Filter != nil {
filter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
filterByNodeReadiness = req.Filter.FilterByNodeReadiness
}
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -131,37 +114,12 @@ func (m *module) getTopNodeGroupsAndMetadata(
return err
})
if len(filterByPodStatus) != 0 {
g.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if len(filterByNodeReadiness) != 0 {
g.Go(func() error {
var err error
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByNodeReadiness)
return err
})
}
if orderByKey == inframonitoringtypes.NodeNameAttrKey {
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, nil, err
}
// Secondary filter: keep only status/readiness-matching groups. A missing
// metric yields an empty statusCounts, so this correctly empties the result
// (the caller also surfaces the warning). Filters compose as AND.
if len(filterByPodStatus) != 0 {
metadataMap = intersectMap(metadataMap, statusCounts)
}
if len(filterByNodeReadiness) != 0 {
metadataMap = intersectMap(metadataMap, nodeConditionCounts)
return nil, nil, err
}
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NodeNameAttrKey)
return pageGroups, metadataMap, statusCounts, statusWarning, nodeConditionCounts, nil
return pageGroups, metadataMap, nil
}
queryNamesForOrderBy := orderByToNodesQueryNames[orderByKey]
@@ -207,23 +165,10 @@ func (m *module) getTopNodeGroupsAndMetadata(
})
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, nil, err
return nil, nil, err
}
// Secondary filter: intersect ranked groups + metadata with the status/readiness
// keyset. A missing metric yields an empty keyset, correctly emptying the result
// (the caller also surfaces the warning). Filters compose as AND.
if len(filterByPodStatus) != 0 {
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
metadataMap = intersectMap(metadataMap, statusCounts)
}
if len(filterByNodeReadiness) != 0 {
allMetricGroups = intersectRankedGroups(allMetricGroups, nodeConditionCounts)
metadataMap = intersectMap(metadataMap, nodeConditionCounts)
}
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
return pageGroups, metadataMap, statusCounts, statusWarning, nodeConditionCounts, nil
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
}
func (m *module) getNodesTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableNodes) (map[string]map[string]string, error) {
@@ -233,11 +178,7 @@ func (m *module) getNodesTableMetadata(ctx context.Context, orgID valuer.UUID, r
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, nodesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, nodesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}
// getPerGroupNodeConditionCounts computes per-group node counts bucketed by each
@@ -251,24 +192,6 @@ func (m *module) getNodesTableMetadata(ctx context.Context, orgID valuer.UUID, r
// countNodesPerCondition: per-group uniqExactIf into ready/not_ready buckets.
//
// Groups absent from the result map have implicit zero counts (caller default).
// applyNodeReadinessFilter adds the readiness push-down (condition_value IN (...))
// to the outer count builder. condition_value is numeric (1=Ready, 0=NotReady), so
// we map each requested enum to its int. No-op when the requested set is empty.
func applyNodeReadinessFilter(cb *sqlbuilder.SelectBuilder, filterByNodeReadiness []inframonitoringtypes.NodeCondition) {
if len(filterByNodeReadiness) == 0 {
return
}
nums := make([]int, len(filterByNodeReadiness))
for i, c := range filterByNodeReadiness {
v := inframonitoringtypes.NodeConditionNumNotReady
if c == inframonitoringtypes.NodeConditionReady {
v = inframonitoringtypes.NodeConditionNumReady
}
nums[i] = v
}
cb.Where(cb.In("condition_value", sqlbuilder.List(nums)))
}
func (m *module) getPerGroupNodeConditionCounts(
ctx context.Context,
orgID valuer.UUID,
@@ -276,11 +199,8 @@ func (m *module) getPerGroupNodeConditionCounts(
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
pageGroups []map[string]string,
filterByNodeReadiness []inframonitoringtypes.NodeCondition,
) (map[string]nodeConditionCounts, error) {
// Empty pageGroups means "span all under user filter", allowed only in
// full-scope mode (filtering by readiness). Otherwise it's an empty page.
if len(groupBy) == 0 || (len(pageGroups) == 0 && len(filterByNodeReadiness) == 0) {
if len(pageGroups) == 0 || len(groupBy) == 0 {
return map[string]nodeConditionCounts{}, nil
}
@@ -368,14 +288,11 @@ func (m *module) getPerGroupNodeConditionCounts(
fmt.Sprintf("uniqExactIf(node_name, condition_value = %d) AS ready_count", inframonitoringtypes.NodeConditionNumReady),
fmt.Sprintf("uniqExactIf(node_name, condition_value = %d) AS not_ready_count", inframonitoringtypes.NodeConditionNumNotReady),
)
// Outer count query. Built with sqlbuilder so the readiness push-down uses a
// proper IN (keep only nodes whose readiness is in the requested set).
countBuilder := sqlbuilder.NewSelectBuilder()
countBuilder.Select(countNodesPerConditionSelectCols...)
countBuilder.From("latest_condition_per_node")
applyNodeReadinessFilter(countBuilder, filterByNodeReadiness)
countBuilder.GroupBy(countNodesPerConditionGroupBy...)
countNodesPerConditionSQL, countArgs := countBuilder.BuildWithFlavor(sqlbuilder.ClickHouse)
countNodesPerConditionSQL := fmt.Sprintf(
"SELECT %s FROM latest_condition_per_node GROUP BY %s",
strings.Join(countNodesPerConditionSelectCols, ", "),
strings.Join(countNodesPerConditionGroupBy, ", "),
)
// Combine CTEs + outer.
cteFragments := []string{
@@ -383,7 +300,7 @@ func (m *module) getPerGroupNodeConditionCounts(
fmt.Sprintf("latest_condition_per_node AS (%s)", latestConditionPerNodeSQL),
}
finalSQL := querybuilder.CombineCTEs(cteFragments) + countNodesPerConditionSQL
finalArgs := querybuilder.PrependArgs([][]any{timeSeriesFPsArgs, latestConditionPerNodeArgs}, countArgs)
finalArgs := querybuilder.PrependArgs([][]any{timeSeriesFPsArgs, latestConditionPerNodeArgs}, nil)
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
if err != nil {

View File

@@ -1,65 +0,0 @@
package implinframonitoring
import (
"strings"
"testing"
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
"github.com/huandu/go-sqlbuilder"
"github.com/stretchr/testify/assert"
)
func TestApplyNodeReadinessFilter(t *testing.T) {
tests := []struct {
name string
readiness []inframonitoringtypes.NodeCondition
wantWhere bool
wantArgs []any
}{
{
name: "empty set yields no clause",
readiness: nil,
wantWhere: false,
wantArgs: nil,
},
{
name: "ready maps to 1 via IN",
readiness: []inframonitoringtypes.NodeCondition{inframonitoringtypes.NodeConditionReady},
wantWhere: true,
wantArgs: []any{inframonitoringtypes.NodeConditionNumReady},
},
{
name: "not_ready maps to 0 via IN",
readiness: []inframonitoringtypes.NodeCondition{inframonitoringtypes.NodeConditionNotReady},
wantWhere: true,
wantArgs: []any{inframonitoringtypes.NodeConditionNumNotReady},
},
{
name: "multiple conditions map to their ints via IN",
readiness: []inframonitoringtypes.NodeCondition{
inframonitoringtypes.NodeConditionReady,
inframonitoringtypes.NodeConditionNotReady,
},
wantWhere: true,
wantArgs: []any{inframonitoringtypes.NodeConditionNumReady, inframonitoringtypes.NodeConditionNumNotReady},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cb := sqlbuilder.NewSelectBuilder()
cb.Select("node_name")
cb.From("latest_condition_per_node")
applyNodeReadinessFilter(cb, tt.readiness)
sql, args := cb.BuildWithFlavor(sqlbuilder.ClickHouse)
hasWhere := strings.Contains(sql, "condition_value IN (")
assert.Equal(t, tt.wantWhere, hasWhere)
if len(tt.wantArgs) == 0 {
assert.Empty(t, args)
} else {
assert.Equal(t, tt.wantArgs, args)
}
})
}
}

View File

@@ -146,34 +146,24 @@ func buildPodRecords(
return records
}
// getTopPodGroupsAndMetadata concurrently fetches metadata + the ordering-metric
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
// getTopPodGroupsAndMetadata fetches the group metadata and the ordering-metric
// ranking concurrently, then pages the ranked groups, backfilling from metadata
// when the page extends past the metric-ranked groups. Returns the page of
// groups and the metadata map (needed by the caller for Total and records).
func (m *module) getTopPodGroupsAndMetadata(
ctx context.Context,
orgID valuer.UUID,
req *inframonitoringtypes.PostablePods,
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
) ([]map[string]string, map[string]map[string]string, error) {
var (
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
statusCounts map[string]podStatusCounts
statusWarning *qbtypes.QueryWarnData
filter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
)
orderByKey = req.OrderBy.Key.Name
// When filtering by pod status, resolve the full-scope status keyset
// concurrently (pageGroups=nil spans all groups under the user filter) so it
// can intersect metadata + ranked groups below.
if req.Filter != nil {
filter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -182,26 +172,12 @@ func (m *module) getTopPodGroupsAndMetadata(
return err
})
if len(filterByPodStatus) != 0 {
g.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if orderByKey == inframonitoringtypes.PodNameAttrKey {
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, err
}
// Secondary filter: keep only status-matching groups. A missing metric
// yields an empty statusCounts, so this correctly empties the result
// (the caller also surfaces the warning).
if len(filterByPodStatus) != 0 {
metadataMap = intersectMap(metadataMap, statusCounts)
return nil, nil, err
}
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.PodNameAttrKey)
return pageGroups, metadataMap, statusCounts, statusWarning, nil
return pageGroups, metadataMap, nil
}
queryNamesForOrderBy := orderByToPodsQueryNames[orderByKey]
@@ -247,19 +223,10 @@ func (m *module) getTopPodGroupsAndMetadata(
})
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, err
return nil, nil, err
}
// Secondary filter: intersect ranked groups + metadata with the status keyset.
// A missing metric yields an empty statusCounts, correctly emptying the result
// (the caller also surfaces the warning).
if len(filterByPodStatus) != 0 {
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
metadataMap = intersectMap(metadataMap, statusCounts)
}
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
return pageGroups, metadataMap, statusCounts, statusWarning, nil
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
}
func (m *module) getPodsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostablePods) (map[string]map[string]string, error) {
@@ -269,11 +236,7 @@ func (m *module) getPodsTableMetadata(ctx context.Context, orgID valuer.UUID, re
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, podsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, podsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}
// getPerGroupPodStatusCountsWithReqMetricChecks gates getPerGroupPodStatusCounts
@@ -288,7 +251,6 @@ func (m *module) getPerGroupPodStatusCountsWithReqMetricChecks(
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
pageGroups []map[string]string,
filterByPodStatus []inframonitoringtypes.PodStatus,
) (map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
present, err := m.getMetricsExistence(ctx, podStatusMetricNamesList)
if err != nil {
@@ -314,28 +276,13 @@ func (m *module) getPerGroupPodStatusCountsWithReqMetricChecks(
return map[string]podStatusCounts{}, warning, nil
}
counts, err := m.getPerGroupPodStatusCounts(ctx, orgID, start, end, filter, groupBy, pageGroups, filterByPodStatus)
counts, err := m.getPerGroupPodStatusCounts(ctx, orgID, start, end, filter, groupBy, pageGroups)
if err != nil {
return nil, nil, err
}
return counts, nil, nil
}
// applyPodStatusFilter adds the display-status push-down (lower(display_status)
// IN (...)) to the outer count builder. valuer lowercases the wire value while
// display_status is kubectl-cased, so we compare lower() on both. No-op when the
// requested set is empty.
func applyPodStatusFilter(cb *sqlbuilder.SelectBuilder, filterByPodStatus []inframonitoringtypes.PodStatus) {
if len(filterByPodStatus) == 0 {
return
}
vals := make([]string, len(filterByPodStatus))
for i, s := range filterByPodStatus {
vals[i] = s.StringValue()
}
cb.Where(cb.In("lower(display_status)", sqlbuilder.List(vals)))
}
// 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).
@@ -356,20 +303,13 @@ func (m *module) getPerGroupPodStatusCounts(
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
pageGroups []map[string]string,
filterByPodStatus []inframonitoringtypes.PodStatus,
) (map[string]podStatusCounts, error) {
// return early if no group by or (no pagegroups provided plus no filterBystatus given for a full scan)
if len(groupBy) == 0 || (len(pageGroups) == 0 && len(filterByPodStatus) == 0) {
if len(pageGroups) == 0 || len(groupBy) == 0 {
return map[string]podStatusCounts{}, nil
}
var (
filterClause *sqlbuilder.WhereClause
err error
userFilterExpr string
)
// Merge user filter with page-groups IN clauses.
userFilterExpr := ""
if filter != nil {
userFilterExpr = filter.Expression
}
@@ -382,7 +322,10 @@ func (m *module) getPerGroupPodStatusCounts(
// 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, orgID, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
if err != nil {
@@ -597,15 +540,11 @@ func (m *module) getPerGroupPodStatusCounts(
countGroupBy = append(countGroupBy, col)
}
countSelectCols = append(countSelectCols, statusCountCols...)
// Outer count query. Built with sqlbuilder so the status push-down uses a
// proper IN (keep only pods whose display status is in the requested set).
countBuilder := sqlbuilder.NewSelectBuilder()
countBuilder.Select(countSelectCols...)
countBuilder.From("pod_status")
applyPodStatusFilter(countBuilder, filterByPodStatus)
countBuilder.GroupBy(countGroupBy...)
countSQL, countArgs := countBuilder.BuildWithFlavor(sqlbuilder.ClickHouse)
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{
@@ -622,7 +561,7 @@ func (m *module) getPerGroupPodStatusCounts(
phaseFpsArgs, phasePerPodArgs,
podReasonFpsArgs, podReasonPerPodArgs,
containerReasonFpsArgs, containerInnerArgs,
}, countArgs)
}, nil)
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
if err != nil {

View File

@@ -1,59 +0,0 @@
package implinframonitoring
import (
"strings"
"testing"
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
"github.com/huandu/go-sqlbuilder"
"github.com/stretchr/testify/assert"
)
func TestApplyPodStatusFilter(t *testing.T) {
tests := []struct {
name string
statuses []inframonitoringtypes.PodStatus
wantWhere bool
wantArgs []any
}{
{
name: "empty set yields no clause",
statuses: nil,
wantWhere: false,
wantArgs: nil,
},
{
name: "single status pushes lowercased arg via IN",
statuses: []inframonitoringtypes.PodStatus{inframonitoringtypes.PodStatusRunning},
wantWhere: true,
wantArgs: []any{"running"},
},
{
name: "multiple statuses push lowercased args via IN",
statuses: []inframonitoringtypes.PodStatus{
inframonitoringtypes.PodStatusRunning,
inframonitoringtypes.PodStatusCrashLoopBackOff,
},
wantWhere: true,
wantArgs: []any{"running", "crashloopbackoff"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cb := sqlbuilder.NewSelectBuilder()
cb.Select("pod_uid")
cb.From("pod_status")
applyPodStatusFilter(cb, tt.statuses)
sql, args := cb.BuildWithFlavor(sqlbuilder.ClickHouse)
hasWhere := strings.Contains(sql, "lower(display_status) IN (")
assert.Equal(t, tt.wantWhere, hasWhere)
if len(tt.wantArgs) == 0 {
assert.Empty(t, args)
} else {
assert.Equal(t, tt.wantArgs, args)
}
})
}
}

View File

@@ -82,34 +82,20 @@ func buildStatefulSetRecords(
return records
}
// getTopStatefulSetGroupsAndMetadata concurrently fetches metadata + the ordering-metric
// ranking (plus the full-scope pod-status keyset when filtering, to intersect both).
func (m *module) getTopStatefulSetGroupsAndMetadata(
ctx context.Context,
orgID valuer.UUID,
req *inframonitoringtypes.PostableStatefulSets,
) ([]map[string]string, map[string]map[string]string, map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
) ([]map[string]string, map[string]map[string]string, error) {
var (
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
statusCounts map[string]podStatusCounts
statusWarning *qbtypes.QueryWarnData
filter *qbtypes.Filter
filterByPodStatus []inframonitoringtypes.PodStatus
orderByKey string
metadataMap map[string]map[string]string
allMetricGroups []rankedGroup
)
orderByKey = req.OrderBy.Key.Name
// When filtering by pod status, resolve the full-scope status keyset
// concurrently (pageGroups=nil spans all groups under the user filter) so it
// can intersect metadata + ranked groups below.
if req.Filter != nil {
filter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -118,26 +104,12 @@ func (m *module) getTopStatefulSetGroupsAndMetadata(
return err
})
if len(filterByPodStatus) != 0 {
g.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, filter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if orderByKey == inframonitoringtypes.StatefulSetNameAttrKey {
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, err
}
// Secondary filter: keep only status-matching groups. A missing metric
// yields an empty statusCounts, so this correctly empties the result
// (the caller also surfaces the warning).
if len(filterByPodStatus) != 0 {
metadataMap = intersectMap(metadataMap, statusCounts)
return nil, nil, err
}
pageGroups := inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.StatefulSetNameAttrKey)
return pageGroups, metadataMap, statusCounts, statusWarning, nil
return pageGroups, metadataMap, nil
}
queryNamesForOrderBy := orderByToStatefulSetsQueryNames[orderByKey]
@@ -183,19 +155,10 @@ func (m *module) getTopStatefulSetGroupsAndMetadata(
})
if err := g.Wait(); err != nil {
return nil, nil, nil, nil, err
return nil, nil, err
}
// Secondary filter: intersect ranked groups + metadata with the status keyset.
// A missing metric yields an empty statusCounts, correctly emptying the result
// (the caller also surfaces the warning).
if len(filterByPodStatus) != 0 {
allMetricGroups = intersectRankedGroups(allMetricGroups, statusCounts)
metadataMap = intersectMap(metadataMap, statusCounts)
}
pageGroups := paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit)
return pageGroups, metadataMap, statusCounts, statusWarning, nil
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), metadataMap, nil
}
func (m *module) getStatefulSetsTableMetadata(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableStatefulSets) (map[string]map[string]string, error) {
@@ -205,9 +168,5 @@ func (m *module) getStatefulSetsTableMetadata(ctx context.Context, orgID valuer.
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, statefulSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, statefulSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}

View File

@@ -36,73 +36,70 @@ type legacyExtraData struct {
}
// newPostableSavedViewFromLegacyView builds a create payload for a v1 request.
// Unlike migration 109 (which processes historical rows it can't ask anyone
// about), this is a live request with a caller on the other end -- malformed
// extraData is rejected outright rather than silently written as a partial row.
func newPostableSavedViewFromLegacyView(v *v3.SavedView) (savedviewtypes.PostableSavedView, error) {
func newPostableSavedViewFromLegacyView(v *v3.SavedView) savedviewtypes.PostableSavedView {
var legacy legacyExtraData
if v.ExtraData != "" {
if err := json.Unmarshal([]byte(v.ExtraData), &legacy); err != nil {
return savedviewtypes.PostableSavedView{}, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse extraData")
}
// Best-effort: malformed/older extraData shapes never fail the request
_ = json.Unmarshal([]byte(v.ExtraData), &legacy)
}
return savedviewtypes.PostableSavedView{
GenerateName: true,
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: v.Name,
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
Queries: v.CompositeQuery.Queries,
SelectedFields: legacy.SelectColumns,
Display: savedviewtypes.Display{
MaxLines: legacy.MaxLines,
FontSize: legacy.FontSize,
Format: legacy.Format,
Color: legacy.Color,
GenerateName: true,
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
Data: savedviewtypes.SavedViewData{
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: v.Name,
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
Queries: v.CompositeQuery.Queries,
SelectedFields: legacy.SelectColumns,
Display: savedviewtypes.Display{
MaxLines: legacy.MaxLines,
FontSize: legacy.FontSize,
Format: legacy.Format,
Color: legacy.Color,
},
},
},
}, nil
}
}
// newUpdatableSavedViewFromLegacyView builds an update payload for a v1 request.
// See newPostableSavedViewFromLegacyView -- same reasoning for rejecting rather
// than swallowing a malformed extraData.
func newUpdatableSavedViewFromLegacyView(v *v3.SavedView) (savedviewtypes.UpdatableSavedView, error) {
func newUpdatableSavedViewFromLegacyView(v *v3.SavedView) savedviewtypes.UpdatableSavedView {
var legacy legacyExtraData
if v.ExtraData != "" {
if err := json.Unmarshal([]byte(v.ExtraData), &legacy); err != nil {
return savedviewtypes.UpdatableSavedView{}, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse extraData")
}
// Best-effort: malformed/older extraData shapes never fail the request
_ = json.Unmarshal([]byte(v.ExtraData), &legacy)
}
return savedviewtypes.UpdatableSavedView{
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: v.Name,
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
Queries: v.CompositeQuery.Queries,
SelectedFields: legacy.SelectColumns,
Display: savedviewtypes.Display{
MaxLines: legacy.MaxLines,
FontSize: legacy.FontSize,
Format: legacy.Format,
Color: legacy.Color,
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
Data: savedviewtypes.SavedViewData{
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: v.Name,
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
Queries: v.CompositeQuery.Queries,
SelectedFields: legacy.SelectColumns,
Display: savedviewtypes.Display{
MaxLines: legacy.MaxLines,
FontSize: legacy.FontSize,
Format: legacy.Format,
Color: legacy.Color,
},
},
},
}, nil
}
}
// newLegacyViewFromSavedView renders a v2 SavedView back into the v1 shape.
func newLegacyViewFromSavedView(v *savedviewtypes.SavedView) (*v3.SavedView, error) {
extraData, err := json.Marshal(legacyExtraData{
Color: v.Spec.Display.Color,
SelectColumns: v.Spec.SelectedFields,
Format: v.Spec.Display.Format,
MaxLines: v.Spec.Display.MaxLines,
FontSize: v.Spec.Display.FontSize,
Color: v.Data.Spec.Display.Color,
SelectColumns: v.Data.Spec.SelectedFields,
Format: v.Data.Spec.Display.Format,
MaxLines: v.Data.Spec.Display.MaxLines,
FontSize: v.Data.Spec.Display.FontSize,
})
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in marshalling extra data")
@@ -110,17 +107,17 @@ func newLegacyViewFromSavedView(v *savedviewtypes.SavedView) (*v3.SavedView, err
return &v3.SavedView{
ID: v.ID,
Name: v.Spec.DisplayName,
Name: v.Data.Spec.DisplayName,
CreatedAt: v.CreatedAt,
CreatedBy: v.CreatedBy,
UpdatedAt: v.UpdatedAt,
UpdatedBy: v.UpdatedBy,
SourcePage: v.Source.StringValue(),
CompositeQuery: &v3.CompositeQuery{
PanelType: v3.PanelType(v.Spec.PanelType.StringValue()),
PanelType: v3.PanelType(v.Data.Spec.PanelType.StringValue()),
// Saved views are only ever created from the explorer's builder mode.
QueryType: v3.QueryTypeBuilder,
Queries: v.Spec.Queries,
Queries: v.Data.Spec.Queries,
},
ExtraData: string(extraData),
}, nil
@@ -159,18 +156,7 @@ func (handler *handler) Create(w http.ResponseWriter, r *http.Request) {
return
}
postable, err := newPostableSavedViewFromLegacyView(&view)
if err != nil {
render.Error(w, err)
return
}
if err := postable.Validate(); err != nil {
render.Error(w, err)
return
}
uuid, err := handler.module.CreateView(ctx, claims.OrgID, postable)
uuid, err := handler.module.CreateView(ctx, claims.OrgID, newPostableSavedViewFromLegacyView(&view))
if err != nil {
render.Error(w, err)
return
@@ -238,22 +224,12 @@ func (handler *handler) Update(w http.ResponseWriter, r *http.Request) {
return
}
updatable, err := newUpdatableSavedViewFromLegacyView(&view)
err = handler.module.UpdateView(ctx, claims.OrgID, viewUUID, newUpdatableSavedViewFromLegacyView(&view))
if err != nil {
render.Error(w, err)
return
}
if err := updatable.Validate(); err != nil {
render.Error(w, err)
return
}
if err := handler.module.UpdateView(ctx, claims.OrgID, viewUUID, updatable); err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, view)
}

View File

@@ -38,18 +38,17 @@ func TestNewPostableSavedViewFromLegacyView(t *testing.T) {
ExtraData: `{"color":"blue","selectColumns":[{"name":"service.name"}],"format":"table","maxLines":10,"fontSize":"large"}`,
}
postable, err := newPostableSavedViewFromLegacyView(legacy)
require.NoError(t, err)
postable := newPostableSavedViewFromLegacyView(legacy)
assert.Empty(t, postable.Name, "v1 has no slug concept -- name must always be generated")
assert.True(t, postable.GenerateName, "v1 has no slug concept -- name must always be generated")
assert.Equal(t, "my view", postable.Spec.DisplayName)
assert.Equal(t, "my view", postable.Data.Spec.DisplayName)
assert.Equal(t, savedviewtypes.SourceLogs, postable.Source)
assert.Equal(t, savedviewtypes.SavedViewSchemaVersion, postable.SchemaVersion)
assert.Equal(t, savedviewtypes.PanelTypeGraph, postable.Spec.PanelType)
assert.Equal(t, legacy.CompositeQuery.Queries, postable.Spec.Queries)
assert.Equal(t, []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}}, postable.Spec.SelectedFields)
assert.Equal(t, savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"}, postable.Spec.Display)
assert.Equal(t, savedviewtypes.SavedViewSchemaVersion, postable.Data.SchemaVersion)
assert.Equal(t, savedviewtypes.PanelTypeGraph, postable.Data.Spec.PanelType)
assert.Equal(t, legacy.CompositeQuery.Queries, postable.Data.Spec.Queries)
assert.Equal(t, []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}}, postable.Data.Spec.SelectedFields)
assert.Equal(t, savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"}, postable.Data.Spec.Display)
})
t.Run("empty extra data leaves display and selected fields zero-valued", func(t *testing.T) {
@@ -63,14 +62,13 @@ func TestNewPostableSavedViewFromLegacyView(t *testing.T) {
ExtraData: "",
}
postable, err := newPostableSavedViewFromLegacyView(legacy)
require.NoError(t, err)
postable := newPostableSavedViewFromLegacyView(legacy)
assert.Equal(t, savedviewtypes.Display{}, postable.Spec.Display)
assert.Nil(t, postable.Spec.SelectedFields)
assert.Equal(t, savedviewtypes.Display{}, postable.Data.Spec.Display)
assert.Nil(t, postable.Data.Spec.SelectedFields)
})
t.Run("malformed extra data is rejected", func(t *testing.T) {
t.Run("malformed extra data is ignored, not an error", func(t *testing.T) {
legacy := &v3.SavedView{
Name: "malformed extra data",
SourcePage: "metrics",
@@ -81,26 +79,10 @@ func TestNewPostableSavedViewFromLegacyView(t *testing.T) {
ExtraData: `{not valid json`,
}
_, err := newPostableSavedViewFromLegacyView(legacy)
assert.Error(t, err)
})
postable := newPostableSavedViewFromLegacyView(legacy)
t.Run("legacy validation gap: empty builderQueries map with no queries", func(t *testing.T) {
legacy := &v3.SavedView{
Name: "no real queries",
SourcePage: "logs",
CompositeQuery: &v3.CompositeQuery{
PanelType: v3.PanelTypeGraph,
QueryType: v3.QueryTypeBuilder,
BuilderQueries: map[string]*v3.BuilderQuery{},
},
}
require.NoError(t, legacy.Validate(), "the legacy CompositeQuery check is expected to miss this")
postable, err := newPostableSavedViewFromLegacyView(legacy)
require.NoError(t, err)
assert.Error(t, postable.Validate(), "the converted postable must catch what the legacy check missed")
assert.Equal(t, "malformed extra data", postable.Data.Spec.DisplayName)
assert.Equal(t, savedviewtypes.Display{}, postable.Data.Spec.Display)
})
}
@@ -115,25 +97,26 @@ func TestNewUpdatableSavedViewFromLegacyView(t *testing.T) {
ExtraData: `{"color":"red"}`,
}
updatable, err := newUpdatableSavedViewFromLegacyView(legacy)
require.NoError(t, err)
updatable := newUpdatableSavedViewFromLegacyView(legacy)
assert.Equal(t, "renamed view", updatable.Spec.DisplayName)
assert.Equal(t, "renamed view", updatable.Data.Spec.DisplayName)
assert.Equal(t, savedviewtypes.SourceTraces, updatable.Source)
}
func TestNewLegacyViewFromSavedView(t *testing.T) {
now := time.Now()
savedView := &savedviewtypes.SavedView{
Name: "my-view-abc123ef",
Source: savedviewtypes.SourceLogs,
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: "my view",
PanelType: savedviewtypes.PanelTypeGraph,
Queries: testQueries(),
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
Display: savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"},
Name: "my-view-abc123ef",
Source: savedviewtypes.SourceLogs,
Data: savedviewtypes.SavedViewData{
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: "my view",
PanelType: savedviewtypes.PanelTypeGraph,
Queries: testQueries(),
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
Display: savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"},
},
},
}
savedView.ID = valuer.GenerateUUID()
@@ -146,7 +129,7 @@ func TestNewLegacyViewFromSavedView(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, savedView.ID, legacy.ID)
assert.Equal(t, savedView.Spec.DisplayName, legacy.Name)
assert.Equal(t, savedView.Data.Spec.DisplayName, legacy.Name)
assert.Equal(t, savedView.CreatedAt, legacy.CreatedAt)
assert.Equal(t, savedView.CreatedBy, legacy.CreatedBy)
assert.Equal(t, savedView.UpdatedAt, legacy.UpdatedAt)
@@ -154,20 +137,20 @@ func TestNewLegacyViewFromSavedView(t *testing.T) {
assert.Equal(t, "logs", legacy.SourcePage)
assert.Equal(t, v3.PanelTypeGraph, legacy.CompositeQuery.PanelType)
assert.Equal(t, v3.QueryTypeBuilder, legacy.CompositeQuery.QueryType)
assert.Equal(t, savedView.Spec.Queries, legacy.CompositeQuery.Queries)
assert.Equal(t, savedView.Data.Spec.Queries, legacy.CompositeQuery.Queries)
var extra legacyExtraData
require.NoError(t, json.Unmarshal([]byte(legacy.ExtraData), &extra))
assert.Equal(t, "blue", extra.Color)
assert.Equal(t, savedView.Spec.SelectedFields, extra.SelectColumns)
assert.Equal(t, savedView.Data.Spec.SelectedFields, extra.SelectColumns)
assert.Equal(t, "table", extra.Format)
assert.Equal(t, 10, extra.MaxLines)
assert.Equal(t, "large", extra.FontSize)
}
func TestNewLegacyViewsFromSavedViews(t *testing.T) {
a := &savedviewtypes.SavedView{Name: "a-slug", Source: savedviewtypes.SourceLogs, Spec: savedviewtypes.SavedViewSpec{DisplayName: "a", PanelType: savedviewtypes.PanelTypeGraph, Queries: testQueries()}}
b := &savedviewtypes.SavedView{Name: "b-slug", Source: savedviewtypes.SourceTraces, Spec: savedviewtypes.SavedViewSpec{DisplayName: "b", PanelType: savedviewtypes.PanelTypeTable, Queries: testQueries()}}
a := &savedviewtypes.SavedView{Name: "a-slug", Source: savedviewtypes.SourceLogs, Data: savedviewtypes.SavedViewData{Spec: savedviewtypes.SavedViewSpec{DisplayName: "a", PanelType: savedviewtypes.PanelTypeGraph, Queries: testQueries()}}}
b := &savedviewtypes.SavedView{Name: "b-slug", Source: savedviewtypes.SourceTraces, Data: savedviewtypes.SavedViewData{Spec: savedviewtypes.SavedViewSpec{DisplayName: "b", PanelType: savedviewtypes.PanelTypeTable, Queries: testQueries()}}}
legacyViews, err := newLegacyViewsFromSavedViews([]*savedviewtypes.SavedView{a, b})
require.NoError(t, err)
@@ -184,58 +167,31 @@ func TestNewLegacyViewsFromSavedViews(t *testing.T) {
// slug (Name) is deliberately NOT part of this contract -- v1 never sees it.
func TestLegacyViewRoundTrip(t *testing.T) {
original := &savedviewtypes.SavedView{
Name: "round-trip-abc123ef",
Source: savedviewtypes.SourceMetrics,
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: "round trip",
PanelType: savedviewtypes.PanelTypeTable,
Queries: testQueries(),
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
Display: savedviewtypes.Display{MaxLines: 5, FontSize: "small", Format: "list", Color: "red"},
Name: "round-trip-abc123ef",
Source: savedviewtypes.SourceMetrics,
Data: savedviewtypes.SavedViewData{
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: "round trip",
PanelType: savedviewtypes.PanelTypeTable,
Queries: testQueries(),
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
Display: savedviewtypes.Display{MaxLines: 5, FontSize: "small", Format: "list", Color: "red"},
},
},
}
legacy, err := newLegacyViewFromSavedView(original)
require.NoError(t, err)
roundTripped, err := newPostableSavedViewFromLegacyView(legacy)
require.NoError(t, err)
roundTripped := newPostableSavedViewFromLegacyView(legacy)
assert.Empty(t, roundTripped.Name)
assert.True(t, roundTripped.GenerateName)
assert.Equal(t, original.Spec.DisplayName, roundTripped.Spec.DisplayName)
assert.Equal(t, original.Data.Spec.DisplayName, roundTripped.Data.Spec.DisplayName)
assert.Equal(t, original.Source, roundTripped.Source)
assert.Equal(t, original.Spec.PanelType, roundTripped.Spec.PanelType)
assert.Equal(t, original.Spec.Queries, roundTripped.Spec.Queries)
assert.Equal(t, original.Spec.SelectedFields, roundTripped.Spec.SelectedFields)
assert.Equal(t, original.Spec.Display, roundTripped.Spec.Display)
}
func TestLegacyViewRoundTrip_EmptySelectedFieldsAndDisplay(t *testing.T) {
original := &savedviewtypes.SavedView{
Name: "round-trip-empty-abc123ef",
Source: savedviewtypes.SourceMetrics,
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: "round trip empty",
PanelType: savedviewtypes.PanelTypeTable,
Queries: testQueries(),
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
Display: savedviewtypes.Display{},
},
}
legacy, err := newLegacyViewFromSavedView(original)
require.NoError(t, err)
var extra legacyExtraData
require.NoError(t, json.Unmarshal([]byte(legacy.ExtraData), &extra))
assert.Nil(t, extra.SelectColumns, "omitempty drops an empty selectColumns from extraData entirely")
roundTripped, err := newPostableSavedViewFromLegacyView(legacy)
require.NoError(t, err)
assert.Empty(t, roundTripped.Spec.SelectedFields, "empty, not necessarily non-nil, on this leg of the round trip")
assert.Equal(t, savedviewtypes.Display{}, roundTripped.Spec.Display)
assert.Equal(t, original.Data.Spec.PanelType, roundTripped.Data.Spec.PanelType)
assert.Equal(t, original.Data.Spec.Queries, roundTripped.Data.Spec.Queries)
assert.Equal(t, original.Data.Spec.SelectedFields, roundTripped.Data.Spec.SelectedFields)
assert.Equal(t, original.Data.Spec.Display, roundTripped.Data.Spec.Display)
}

View File

@@ -28,22 +28,24 @@ func newTestStore() (savedview.Module, *savedviewtypestest.StoreTest) {
func testPostableSavedView(name string, source savedviewtypes.Source) savedviewtypes.PostableSavedView {
return savedviewtypes.PostableSavedView{
Name: name,
Source: source,
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: name,
PanelType: savedviewtypes.PanelTypeGraph,
Queries: []qbtypes.QueryEnvelope{
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Aggregations: []qbtypes.LogAggregation{{Expression: "count()"}},
Name: name,
Source: source,
Data: savedviewtypes.SavedViewData{
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: name,
PanelType: savedviewtypes.PanelTypeGraph,
Queries: []qbtypes.QueryEnvelope{
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Aggregations: []qbtypes.LogAggregation{{Expression: "count()"}},
},
},
},
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
},
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
},
}
}
@@ -51,9 +53,8 @@ func testPostableSavedView(name string, source savedviewtypes.Source) savedviewt
func testUpdatableSavedView(displayName string, source savedviewtypes.Source) savedviewtypes.UpdatableSavedView {
postable := testPostableSavedView(displayName, source)
return savedviewtypes.UpdatableSavedView{
Source: postable.Source,
SchemaVersion: postable.SchemaVersion,
Spec: postable.Spec,
Source: postable.Source,
Data: postable.Data,
}
}
@@ -92,7 +93,7 @@ func TestModule_CreateAndGetView(t *testing.T) {
assert.Equal(t, savedviewtypes.SourceLogs, got.Source)
assert.Equal(t, "creator@signoz.io", got.CreatedBy)
assert.Equal(t, "creator@signoz.io", got.UpdatedBy)
assert.Equal(t, savedviewtypes.PanelTypeGraph, got.Spec.PanelType)
assert.Equal(t, savedviewtypes.PanelTypeGraph, got.Data.Spec.PanelType)
require.NoError(t, st.AssertExpectations())
}
@@ -137,21 +138,21 @@ func TestModule_UpdateView(t *testing.T) {
existingName := existing.Name
updated := testUpdatableSavedView("renamed", savedviewtypes.SourceTraces)
updated.Spec.PanelType = savedviewtypes.PanelTypeTable
updated.Data.Spec.PanelType = savedviewtypes.PanelTypeTable
st.ExpectUpdate(orgID, id, 1)
require.NoError(t, m.UpdateView(contextWithClaims(orgID, "updater@signoz.io"), orgID, id, updated))
stored := testSavedView(orgID, id, "updater@signoz.io", testPostableSavedView("renamed", savedviewtypes.SourceTraces))
stored.Name = existingName
stored.Spec.PanelType = savedviewtypes.PanelTypeTable
stored.Data.Spec.PanelType = savedviewtypes.PanelTypeTable
st.ExpectGet(orgID, id, stored)
got, err := m.GetView(contextWithClaims(orgID, "creator@signoz.io"), orgID, id)
require.NoError(t, err)
assert.Equal(t, existingName, got.Name, "name must not change on update")
assert.Equal(t, "renamed", got.Spec.DisplayName)
assert.Equal(t, "renamed", got.Data.Spec.DisplayName)
assert.Equal(t, savedviewtypes.SourceTraces, got.Source)
assert.Equal(t, savedviewtypes.PanelTypeTable, got.Spec.PanelType)
assert.Equal(t, savedviewtypes.PanelTypeTable, got.Data.Spec.PanelType)
assert.Equal(t, "updater@signoz.io", got.UpdatedBy)
require.NoError(t, st.AssertExpectations())

View File

@@ -19,8 +19,7 @@ func NewStore(sqlstore sqlstore.SQLStore) savedviewtypes.Store {
}
func (store *store) Create(ctx context.Context, view *savedviewtypes.SavedView) error {
storable := savedviewtypes.NewStorableSavedView(view)
_, err := store.sqlstore.BunDB().NewInsert().Model(storable).Exec(ctx)
_, err := store.sqlstore.BunDB().NewInsert().Model(view).Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "saved view with name %s already exists", view.Name)
}
@@ -28,25 +27,23 @@ func (store *store) Create(ctx context.Context, view *savedviewtypes.SavedView)
}
func (store *store) Get(ctx context.Context, orgID string, id valuer.UUID) (*savedviewtypes.SavedView, error) {
var storable savedviewtypes.StorableSavedView
err := store.sqlstore.BunDB().NewSelect().Model(&storable).Where("org_id = ? AND id = ?", orgID, id.StringValue()).Scan(ctx)
var view savedviewtypes.SavedView
err := store.sqlstore.BunDB().NewSelect().Model(&view).Where("org_id = ? AND id = ?", orgID, id.StringValue()).Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, savedviewtypes.ErrCodeSavedViewNotFound, "saved view %s not found", id.StringValue())
}
view := storable.ToSavedView()
normalizeSelectedFields(view)
return view, nil
normalizeSelectedFields(&view)
return &view, nil
}
func (store *store) Update(ctx context.Context, view *savedviewtypes.SavedView) error {
storable := savedviewtypes.NewStorableSavedView(view)
res, err := store.sqlstore.BunDB().NewUpdate().
Model((*savedviewtypes.StorableSavedView)(nil)).
Model(&savedviewtypes.SavedView{}).
Set("updated_at = ?, updated_by = ?, source = ?, data = ?",
storable.UpdatedAt, storable.UpdatedBy, storable.Source, storable.Data).
Where("id = ?", storable.ID.StringValue()).
Where("org_id = ?", storable.OrgID).
view.UpdatedAt, view.UpdatedBy, view.Source, view.Data).
Where("id = ?", view.ID.StringValue()).
Where("org_id = ?", view.OrgID).
Exec(ctx)
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "error in updating saved view")
@@ -65,7 +62,7 @@ func (store *store) Update(ctx context.Context, view *savedviewtypes.SavedView)
func (store *store) Delete(ctx context.Context, orgID string, id valuer.UUID) error {
res, err := store.sqlstore.BunDB().NewDelete().
Model((*savedviewtypes.StorableSavedView)(nil)).
Model(&savedviewtypes.SavedView{}).
Where("id = ?", id.StringValue()).
Where("org_id = ?", orgID).
Exec(ctx)
@@ -85,8 +82,8 @@ func (store *store) Delete(ctx context.Context, orgID string, id valuer.UUID) er
}
func (store *store) List(ctx context.Context, orgID string, source savedviewtypes.Source, name string) ([]*savedviewtypes.SavedView, error) {
var storables []*savedviewtypes.StorableSavedView
q := store.sqlstore.BunDB().NewSelect().Model(&storables).
var views []*savedviewtypes.SavedView
q := store.sqlstore.BunDB().NewSelect().Model(&views).
Where("org_id = ?", orgID).
Where("name LIKE ?", "%"+name+"%")
if !source.IsZero() {
@@ -97,11 +94,8 @@ func (store *store) List(ctx context.Context, orgID string, source savedviewtype
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in getting saved views")
}
views := make([]*savedviewtypes.SavedView, 0, len(storables))
for _, storable := range storables {
view := storable.ToSavedView()
for _, view := range views {
normalizeSelectedFields(view)
views = append(views, view)
}
return views, nil
@@ -109,7 +103,7 @@ func (store *store) List(ctx context.Context, orgID string, source savedviewtype
// normalizeSelectedFields fixes up a scanned row's nil SelectedFields.
func normalizeSelectedFields(view *savedviewtypes.SavedView) {
if view.Spec.SelectedFields == nil {
view.Spec.SelectedFields = []telemetrytypes.TelemetryFieldKey{}
if view.Data.Spec.SelectedFields == nil {
view.Data.Spec.SelectedFields = []telemetrytypes.TelemetryFieldKey{}
}
}

View File

@@ -0,0 +1,9 @@
package prometheus
import "net/http"
type Handler interface {
Query(http.ResponseWriter, *http.Request)
QueryRange(http.ResponseWriter, *http.Request)
}

View File

@@ -0,0 +1,259 @@
// Package promapi serves the Prometheus HTTP query API over a
// prometheus.Prometheus provider: /query and /query_range in the shape of
// Prometheus' /api/v1 endpoints (https://prometheus.io/docs/prometheus/latest/querying/api/),
// intended to be mounted under a distinguishing prefix (/prometheus/api/v1)
// so PromQL-only endpoints are separate from the SigNoz query APIs. The
// request and response contracts follow Prometheus: form-encoded GET/POST
// params, {"status":"success","data":{resultType,result}} on success and
// {"status":"error","errorType","error"} with Prometheus' status codes on
// failure — so Prometheus-compatible clients can point at the prefix.
package promapi
import (
"context"
"encoding/json"
"log/slog"
"math"
"net/http"
"strconv"
"time"
promModel "github.com/prometheus/common/model"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/util/stats"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/prometheus"
)
type handler struct {
logger *slog.Logger
prom prometheus.Prometheus
}
func NewHandler(logger *slog.Logger, prom prometheus.Prometheus) prometheus.Handler {
return &handler{logger: logger, prom: prom}
}
type errorType string
const (
errBadData errorType = "bad_data"
errExec errorType = "execution"
errCanceled errorType = "canceled"
errTimeout errorType = "timeout"
errInternal errorType = "internal"
)
type queryData struct {
ResultType parser.ValueType `json:"resultType"`
Result parser.Value `json:"result"`
Stats stats.QueryStats `json:"stats,omitempty"`
}
type response struct {
Status string `json:"status"`
Data *queryData `json:"data,omitempty"`
ErrorType errorType `json:"errorType,omitempty"`
Error string `json:"error,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Infos []string `json:"infos,omitempty"`
}
// QueryRange evaluates an expression over a grid: query, start, end, step,
// and optional timeout/stats params, all in Prometheus' formats.
func (h *handler) QueryRange(w http.ResponseWriter, r *http.Request) {
start, err := parseTime(r.FormValue("start"))
if err != nil {
h.respondError(r.Context(), w, errBadData, err)
return
}
end, err := parseTime(r.FormValue("end"))
if err != nil {
h.respondError(r.Context(), w, errBadData, err)
return
}
if end.Before(start) {
h.respondError(r.Context(), w, errBadData, errors.NewInvalidInputf(errors.CodeInvalidInput, "end timestamp must not be before start time"))
return
}
step, err := parseDuration(r.FormValue("step"))
if err != nil {
h.respondError(r.Context(), w, errBadData, err)
return
}
if step <= 0 {
h.respondError(r.Context(), w, errBadData, errors.NewInvalidInputf(errors.CodeInvalidInput, "zero or negative query resolution step widths are not accepted. Try a positive integer"))
return
}
// The engine materializes every point of every series; an unbounded
// grid is an unbounded allocation. 11,000 points covers 60s resolution
// for a week or 1h resolution for a year.
if end.Sub(start)/step > 11000 {
h.respondError(r.Context(), w, errBadData, errors.NewInvalidInputf(errors.CodeInvalidInput, "exceeded maximum resolution of 11,000 points per timeseries. Try decreasing the query resolution (?step=XX)"))
return
}
ctx, cancel, err := h.contextWithTimeout(r)
if err != nil {
h.respondError(r.Context(), w, errBadData, err)
return
}
defer cancel()
if h.tryRangeExecutor(ctx, w, r, start, end, step) {
return
}
qry, err := h.prom.Engine().NewRangeQuery(ctx, h.prom.Storage(), nil, r.FormValue("query"), start, end, step)
if err != nil {
h.respondError(r.Context(), w, errBadData, err)
return
}
h.exec(ctx, w, r, qry)
}
// tryRangeExecutor serves the query the way a RangeExecutor provider is
// designed to serve: evaluated inside the datastore when the shape allows.
// It reports whether the response was written.
func (h *handler) tryRangeExecutor(ctx context.Context, w http.ResponseWriter, r *http.Request, start, end time.Time, step time.Duration) bool {
re, ok := h.prom.(prometheus.RangeExecutor)
if !ok {
return false
}
matrix, served, err := re.TryExecuteRange(ctx, r.FormValue("query"), start, end, step)
if err != nil {
h.respondError(ctx, w, errExec, err)
return true
}
if !served {
return false
}
h.respond(ctx, w, &queryData{ResultType: matrix.Type(), Result: matrix}, nil, nil)
return true
}
// Query evaluates an expression at a single instant: query and optional
// time/timeout/stats params. A missing time evaluates at the server's now,
// as in Prometheus.
func (h *handler) Query(w http.ResponseWriter, r *http.Request) {
ts := time.Now()
if t := r.FormValue("time"); t != "" {
var err error
ts, err = parseTime(t)
if err != nil {
h.respondError(r.Context(), w, errBadData, err)
return
}
}
ctx, cancel, err := h.contextWithTimeout(r)
if err != nil {
h.respondError(r.Context(), w, errBadData, err)
return
}
defer cancel()
qry, err := h.prom.Engine().NewInstantQuery(ctx, h.prom.Storage(), nil, r.FormValue("query"), ts)
if err != nil {
h.respondError(r.Context(), w, errBadData, err)
return
}
h.exec(ctx, w, r, qry)
}
func (h *handler) exec(ctx context.Context, w http.ResponseWriter, r *http.Request, qry promql.Query) {
defer qry.Close()
res := qry.Exec(ctx)
if res.Err != nil {
h.logger.ErrorContext(ctx, "error evaluating promql query", errors.Attr(res.Err))
switch res.Err.(type) {
case promql.ErrQueryCanceled:
h.respondError(ctx, w, errCanceled, res.Err)
case promql.ErrQueryTimeout:
h.respondError(ctx, w, errTimeout, res.Err)
case promql.ErrStorage:
h.respondError(ctx, w, errInternal, res.Err)
default:
h.respondError(ctx, w, errExec, res.Err)
}
return
}
data := &queryData{ResultType: res.Value.Type(), Result: res.Value}
if r.FormValue("stats") != "" {
data.Stats = stats.NewQueryStats(qry.Stats())
}
warnings, infos := res.Warnings.AsStrings(r.FormValue("query"), 10, 10)
h.respond(ctx, w, data, warnings, infos)
}
func (h *handler) contextWithTimeout(r *http.Request) (context.Context, context.CancelFunc, error) {
ctx := r.Context()
if to := r.FormValue("timeout"); to != "" {
timeout, err := parseDuration(to)
if err != nil {
return nil, nil, err
}
ctx, cancel := context.WithTimeout(ctx, timeout)
return ctx, cancel, nil
}
ctx, cancel := context.WithCancel(ctx)
return ctx, cancel, nil
}
func (h *handler) respond(ctx context.Context, w http.ResponseWriter, data *queryData, warnings, infos []string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(&response{Status: "success", Data: data, Warnings: warnings, Infos: infos}); err != nil {
h.logger.ErrorContext(ctx, "error writing prometheus api response", errors.Attr(err))
}
}
// respondError follows Prometheus' status-code mapping: bad_data 400,
// execution 422, canceled/timeout 503, internal 500.
func (h *handler) respondError(ctx context.Context, w http.ResponseWriter, typ errorType, err error) {
code := http.StatusInternalServerError
switch typ {
case errBadData:
code = http.StatusBadRequest
case errExec:
code = http.StatusUnprocessableEntity
case errCanceled, errTimeout:
code = http.StatusServiceUnavailable
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
if encErr := json.NewEncoder(w).Encode(&response{Status: "error", ErrorType: typ, Error: err.Error()}); encErr != nil {
h.logger.ErrorContext(ctx, "error writing prometheus api error response", errors.Attr(encErr))
}
}
// parseTime accepts Prometheus' time formats: float unix seconds or RFC3339.
func parseTime(s string) (time.Time, error) {
if t, err := strconv.ParseFloat(s, 64); err == nil {
sec, ns := math.Modf(t)
return time.Unix(int64(sec), int64(ns*float64(time.Second))), nil
}
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
return t, nil
}
return time.Time{}, errors.NewInvalidInputf(errors.CodeInvalidInput, "cannot parse %q to a valid timestamp", s)
}
// parseDuration accepts Prometheus' duration formats: float seconds or a
// duration string like 5m.
func parseDuration(s string) (time.Duration, error) {
if d, err := strconv.ParseFloat(s, 64); err == nil {
ts := d * float64(time.Second)
if ts > float64(math.MaxInt64) || ts < float64(math.MinInt64) {
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "cannot parse %q to a valid duration. It overflows int64", s)
}
return time.Duration(ts), nil
}
if d, err := promModel.ParseDuration(s); err == nil {
return time.Duration(d), nil
}
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "cannot parse %q to a valid duration", s)
}

View File

@@ -23,11 +23,7 @@ func (q *querier) Collect(ctx context.Context, _ valuer.UUID) (map[string]any, e
traces uint64
tracesLastSeenAt time.Time
)
tracesLastSeenExpr := "max(timestamp)"
if q.hasColumn(ctx, tracestelemetryschema.DBName, tracestelemetryschema.SpanIndexV3TableName, "inserted_at") {
tracesLastSeenExpr = "max(inserted_at)"
}
if err := q.telemetryStore.ClickhouseDB().QueryRow(ctx, fmt.Sprintf("SELECT COUNT(*), %s FROM %s", tracesLastSeenExpr, tracesTable)).Scan(&traces, &tracesLastSeenAt); err == nil {
if err := q.telemetryStore.ClickhouseDB().QueryRow(ctx, fmt.Sprintf("SELECT COUNT(*), max(timestamp) FROM %s", tracesTable)).Scan(&traces, &tracesLastSeenAt); err == nil {
stats["telemetry.traces.count"] = traces
if tracesLastSeenAt.Unix() != 0 {
stats["telemetry.traces.last_observed.time"] = tracesLastSeenAt.UTC()
@@ -41,11 +37,7 @@ func (q *querier) Collect(ctx context.Context, _ valuer.UUID) (map[string]any, e
logs uint64
logsLastSeenAt time.Time
)
logsLastSeenExpr := "fromUnixTimestamp64Nano(max(timestamp))"
if q.hasColumn(ctx, logstelemetryschema.DBName, logstelemetryschema.LogsV2TableName, "inserted_at") {
logsLastSeenExpr = "max(inserted_at)"
}
if err := q.telemetryStore.ClickhouseDB().QueryRow(ctx, fmt.Sprintf("SELECT COUNT(*), %s FROM %s", logsLastSeenExpr, logsTable)).Scan(&logs, &logsLastSeenAt); err == nil {
if err := q.telemetryStore.ClickhouseDB().QueryRow(ctx, fmt.Sprintf("SELECT COUNT(*), fromUnixTimestamp64Nano(max(timestamp)) FROM %s", logsTable)).Scan(&logs, &logsLastSeenAt); err == nil {
stats["telemetry.logs.count"] = logs
if logsLastSeenAt.Unix() != 0 {
stats["telemetry.logs.last_observed.time"] = logsLastSeenAt.UTC()
@@ -59,11 +51,7 @@ func (q *querier) Collect(ctx context.Context, _ valuer.UUID) (map[string]any, e
metrics uint64
metricsLastSeenAt time.Time
)
metricsLastSeenExpr := "toDateTime(max(unix_milli) / 1000)"
if q.hasColumn(ctx, metricstelemetryschema.DBName, metricstelemetryschema.SamplesV4TableName, "inserted_at_unix_milli") {
metricsLastSeenExpr = "fromUnixTimestamp64Milli(max(inserted_at_unix_milli))"
}
if err := q.telemetryStore.ClickhouseDB().QueryRow(ctx, fmt.Sprintf("SELECT COUNT(*), %s FROM %s", metricsLastSeenExpr, metricsTable)).Scan(&metrics, &metricsLastSeenAt); err == nil {
if err := q.telemetryStore.ClickhouseDB().QueryRow(ctx, fmt.Sprintf("SELECT COUNT(*), toDateTime(max(unix_milli) / 1000) FROM %s", metricsTable)).Scan(&metrics, &metricsLastSeenAt); err == nil {
stats["telemetry.metrics.count"] = metrics
if metricsLastSeenAt.Unix() != 0 {
stats["telemetry.metrics.last_observed.time"] = metricsLastSeenAt.UTC()
@@ -75,12 +63,3 @@ func (q *querier) Collect(ctx context.Context, _ valuer.UUID) (map[string]any, e
return stats, nil
}
func (q *querier) hasColumn(ctx context.Context, database, table, column string) bool {
var exists bool
if err := q.telemetryStore.ClickhouseDB().QueryRow(ctx, "SELECT hasColumnInTable(?, ?, ?)", database, table, column).Scan(&exists); err != nil {
q.logger.DebugContext(ctx, "failed to check column existence", errors.Attr(err))
return false
}
return exists
}

View File

@@ -485,6 +485,9 @@ func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) {
func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/query_range", am.ViewAccess(aH.queryRangeMetrics)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/query", am.ViewAccess(aH.queryMetrics)).Methods(http.MethodGet)
router.HandleFunc("/prometheus/api/v1/query_range", am.ViewAccess(aH.Signoz.Handlers.PrometheusHandler.QueryRange)).Methods(http.MethodGet, http.MethodPost)
router.HandleFunc("/prometheus/api/v1/query", am.ViewAccess(aH.Signoz.Handlers.PrometheusHandler.Query)).Methods(http.MethodGet, http.MethodPost)
router.HandleFunc("/api/v1/rules", am.ViewAccess(aH.listRules)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules/{id}", am.ViewAccess(aH.getRule)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules", am.EditAccess(aH.createRule)).Methods(http.MethodPost)

View File

@@ -1,22 +0,0 @@
// Code generated by scripts/semconv. DO NOT EDIT.
package semconv
var families = []Family{
{
Current: "db.system.name",
Old: []string{"db.system"},
Kind: KindAttribute,
Contexts: nil,
Signals: nil,
ApplyToMetrics: nil,
},
{
Current: "deployment.environment.name",
Old: []string{"deployment.environment"},
Kind: KindAttribute,
Contexts: nil,
Signals: nil,
ApplyToMetrics: nil,
},
}

View File

@@ -1,127 +0,0 @@
package semconv
import (
"slices"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
//go:generate go run ../../scripts/semconv
// Kind identifies whether a family describes an attribute or a metric name.
type Kind struct {
valuer.String
}
// Family is one logical telemetry field. Old is ordered from the most recent
// predecessor to the oldest one and therefore also defines fallback order.
type Family struct {
Current string
Old []string
Kind Kind
Contexts []telemetrytypes.FieldContext
Signals []telemetrytypes.Signal
ApplyToMetrics []string
ValueMap map[string]string
}
var (
KindAttribute = Kind{String: valuer.NewString("attribute")}
KindMetric = Kind{String: valuer.NewString("metric")}
)
var memberToFamilies, familyMembers = buildIndexes()
// Enum returns the acceptable values for Kind.
func (Kind) Enum() []any {
return []any{KindAttribute, KindMetric}
}
// Lookup returns the enabled family containing selector.Name for kind. The
// returned family must not be modified.
func Lookup(kind Kind, selector telemetrytypes.FieldKeySelector) (Family, bool) {
idx, ok := lookupIndex(kind, selector)
if !ok {
return Family{}, false
}
return families[idx], true
}
// Members returns the current name first, followed by historical names in
// fallback order. A name outside an enabled family is returned unchanged. The
// returned slice must not be modified.
func Members(kind Kind, selector telemetrytypes.FieldKeySelector) []string {
idx, ok := lookupIndex(kind, selector)
if !ok {
return []string{selector.Name}
}
return familyMembers[idx]
}
// Current returns the current name for selector.Name, or the input name when
// it does not belong to an enabled family.
func Current(kind Kind, selector telemetrytypes.FieldKeySelector) string {
idx, ok := lookupIndex(kind, selector)
if !ok {
return selector.Name
}
return families[idx].Current
}
// All returns every enabled family. The returned slice and families must not be
// modified.
func All() []Family {
return families
}
func buildIndexes() (map[string][]int, [][]string) {
index := make(map[string][]int)
members := make([][]string, len(families))
for i, family := range families {
members[i] = make([]string, 0, len(family.Old)+1)
members[i] = append(members[i], family.Current)
members[i] = append(members[i], family.Old...)
index[family.Current] = append(index[family.Current], i)
for _, old := range family.Old {
index[old] = append(index[old], i)
}
}
return index, members
}
func lookupIndex(kind Kind, selector telemetrytypes.FieldKeySelector) (int, bool) {
for _, idx := range memberToFamilies[selector.Name] {
if matchesSelector(families[idx], kind, selector) {
return idx, true
}
}
return 0, false
}
func matchesSelector(family Family, kind Kind, selector telemetrytypes.FieldKeySelector) bool {
if family.Kind != kind {
return false
}
if selector.Signal != telemetrytypes.SignalUnspecified && len(family.Signals) > 0 {
if !slices.Contains(family.Signals, selector.Signal) {
return false
}
}
if selector.FieldContext != telemetrytypes.FieldContextUnspecified && len(family.Contexts) > 0 {
if !slices.Contains(family.Contexts, selector.FieldContext) {
return false
}
}
if selector.Signal == telemetrytypes.SignalMetrics && len(family.ApplyToMetrics) > 0 {
if selector.MetricContext == nil {
return false
}
return slices.Contains(family.ApplyToMetrics, selector.MetricContext.MetricName)
}
return true
}

View File

@@ -1,80 +0,0 @@
package semconv
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
)
func TestMembersReturnsCurrentBeforeHistoricalName(t *testing.T) {
selector := telemetrytypes.FieldKeySelector{
Name: "deployment.environment",
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextResource,
}
assert.Equal(t,
[]string{"deployment.environment.name", "deployment.environment"},
Members(KindAttribute, selector),
"members should use current-first fallback order",
)
}
func TestCurrentReturnsCanonicalName(t *testing.T) {
selector := telemetrytypes.FieldKeySelector{
Name: "deployment.environment",
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextResource,
}
assert.Equal(t,
"deployment.environment.name",
Current(KindAttribute, selector),
"historical name should resolve to the current family name",
)
}
func TestAllScopedFamilyMatchesSupportedScopes(t *testing.T) {
tests := []struct {
name string
signal telemetrytypes.Signal
fieldContext telemetrytypes.FieldContext
}{
{name: "trace resource", signal: telemetrytypes.SignalTraces, fieldContext: telemetrytypes.FieldContextResource},
{name: "trace attribute", signal: telemetrytypes.SignalTraces, fieldContext: telemetrytypes.FieldContextAttribute},
{name: "log resource", signal: telemetrytypes.SignalLogs, fieldContext: telemetrytypes.FieldContextResource},
{name: "log attribute", signal: telemetrytypes.SignalLogs, fieldContext: telemetrytypes.FieldContextAttribute},
{name: "metric resource", signal: telemetrytypes.SignalMetrics, fieldContext: telemetrytypes.FieldContextResource},
{name: "metric attribute", signal: telemetrytypes.SignalMetrics, fieldContext: telemetrytypes.FieldContextAttribute},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
selector := telemetrytypes.FieldKeySelector{
Name: "deployment.environment",
Signal: test.signal,
FieldContext: test.fieldContext,
}
assert.Equal(t,
"deployment.environment.name",
Current(KindAttribute, selector),
"an all-scoped family should match every supported signal and attribute context",
)
})
}
}
func TestMembersReturnsInputWhenKindDoesNotMatch(t *testing.T) {
selector := telemetrytypes.FieldKeySelector{
Name: "deployment.environment",
Signal: telemetrytypes.SignalTraces,
}
assert.Equal(t,
[]string{"deployment.environment"},
Members(KindMetric, selector),
"an attribute family must not match a metric-name lookup",
)
}

View File

@@ -48,6 +48,8 @@ import (
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/prometheus/promapi"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/ruler"
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
@@ -81,6 +83,7 @@ type Handlers struct {
RuleStateHistory rulestatehistory.Handler
SpanMapperHandler spanmapper.Handler
AlertmanagerHandler alertmanager.Handler
PrometheusHandler prometheus.Handler
TraceDetail tracedetail.Handler
RulerHandler ruler.Handler
LLMPricingRuleHandler llmpricingrule.Handler
@@ -101,6 +104,7 @@ func NewHandlers(
zeusService zeus.Zeus,
registryHandler factory.Handler,
alertmanagerService alertmanager.Alertmanager,
prometheusService prometheus.Prometheus,
rulerService ruler.Ruler,
statsAggregator statsreporter.Aggregator,
) Handlers {
@@ -129,6 +133,7 @@ func NewHandlers(
CloudIntegrationHandler: implcloudintegration.NewHandler(modules.CloudIntegration),
SpanMapperHandler: implspanmapper.NewHandler(modules.SpanMapper),
AlertmanagerHandler: signozalertmanager.NewHandler(alertmanagerService),
PrometheusHandler: promapi.NewHandler(providerSettings.Logger, prometheusService),
TraceDetail: impltracedetail.NewHandler(modules.TraceDetail),
RulerHandler: signozruler.NewHandler(rulerService),
LLMPricingRuleHandler: impllmpricingrule.NewHandler(modules.LLMPricingRule),

View File

@@ -63,7 +63,7 @@ func TestNewHandlers(t *testing.T) {
querierHandler := querier.NewHandler(providerSettings, nil, nil)
registryHandler := factory.NewHandler(nil)
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler, alertmanager, nil, nil)
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler, alertmanager, nil, nil, nil)
reflectVal := reflect.ValueOf(handlers)
for i := 0; i < reflectVal.NumField(); i++ {
f := reflectVal.Field(i)

View File

@@ -237,7 +237,6 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddDashboardTuplesFactory(sqlstore),
sqlmigration.NewRestructureSavedViewSpecFactory(sqlstore, sqlschema),
sqlmigration.NewAddSavedViewTuplesFactory(sqlstore),
sqlmigration.NewFixSavedViewSelectedFieldsFactory(sqlstore),
)
}

View File

@@ -617,7 +617,7 @@ func New(
// Initialize all handlers for the modules
registryHandler := factory.NewHandler(registry)
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager, rulerInstance, statsAggregator)
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager, prometheus, rulerInstance, statsAggregator)
// Initialize the API server (after registry so it can access service health)
apiserverInstance, err := factory.NewProviderFromNamedMap(

View File

@@ -15,8 +15,6 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
type restructureSavedViewSpec struct {
@@ -45,11 +43,11 @@ type legacySavedViewCompositeQuery struct {
// legacySavedViewExtraData mirrors the frontend defined extraData JSON shape.
type legacySavedViewExtraData struct {
Color string `json:"color,omitempty"`
SelectColumns []telemetrytypes.TelemetryFieldKey `json:"selectColumns,omitempty"`
Format string `json:"format,omitempty"`
MaxLines int `json:"maxLines,omitempty"`
FontSize string `json:"fontSize,omitempty"`
Color string `json:"color,omitempty"`
SelectColumns json.RawMessage `json:"selectColumns,omitempty"`
Format string `json:"format,omitempty"`
MaxLines int `json:"maxLines,omitempty"`
FontSize string `json:"fontSize,omitempty"`
}
type savedViewDisplay struct {
@@ -60,11 +58,11 @@ type savedViewDisplay struct {
}
type savedViewSpec struct {
DisplayName string `json:"displayName"`
PanelType string `json:"panelType"`
Queries json.RawMessage `json:"queries"`
SelectedFields []telemetrytypes.TelemetryFieldKey `json:"selectedFields"`
Display savedViewDisplay `json:"display"`
DisplayName string `json:"displayName"`
PanelType string `json:"panelType"`
Queries json.RawMessage `json:"queries"`
SelectedFields json.RawMessage `json:"selectedFields"`
Display savedViewDisplay `json:"display"`
}
type savedViewData struct {
@@ -166,15 +164,6 @@ func (migration *restructureSavedViewSpec) Up(ctx context.Context, db *bun.DB) e
return err
}
var orgIDs []string
if err := tx.NewSelect().Model((*types.Organization)(nil)).Column("id").Scan(ctx, &orgIDs); err != nil {
return err
}
validOrgIDs := make(map[string]struct{}, len(orgIDs))
for _, id := range orgIDs {
validOrgIDs[id] = struct{}{}
}
// drop table `saved_views`
for _, sql := range migration.sqlschema.Operator().DropTable(savedViewsTable) {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
@@ -221,13 +210,6 @@ func (migration *restructureSavedViewSpec) Up(ctx context.Context, db *bun.DB) e
continue // orphaned row from a pre-existing org_id backfill gap; nothing sane to attach it to
}
// to avoid foreign key constraint issues
if _, ok := validOrgIDs[old.OrgID]; !ok {
skipped++
migration.settings.Logger.WarnContext(ctx, "saved view references an org that no longer exists, skipping", slog.String("org_id", old.OrgID), slog.String("saved_view_id", old.ID))
continue
}
var compositeQuery legacySavedViewCompositeQuery
if err := json.Unmarshal([]byte(old.Data), &compositeQuery); err != nil {
failed++
@@ -239,9 +221,7 @@ func (migration *restructureSavedViewSpec) Up(ctx context.Context, db *bun.DB) e
if old.ExtraData != "" {
// best-effort: malformed/older extraData shapes never fail the migration,
// they just leave selectedFields/display empty.
if err := json.Unmarshal([]byte(old.ExtraData), &extraData); err != nil {
migration.settings.Logger.WarnContext(ctx, "failed to unmarshal saved view extra data, continuing with empty selectedFields/display", slog.String("org_id", old.OrgID), slog.String("saved_view_id", old.ID), slog.Any("error", err))
}
_ = json.Unmarshal([]byte(old.ExtraData), &extraData)
}
dataJSON, err := json.Marshal(savedViewData{

View File

@@ -1,185 +0,0 @@
package sqlmigration
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
type fixSavedViewSelectedFields struct {
sqlstore sqlstore.SQLStore
settings factory.ProviderSettings
}
func NewFixSavedViewSelectedFieldsFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("fix_saved_view_selected_fields"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &fixSavedViewSelectedFields{sqlstore: sqlstore, settings: ps}, nil
})
}
func (migration *fixSavedViewSelectedFields) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
// storableSavedViewData is the shape of the `saved_view` table this migration repairs.
type storableSavedViewData struct {
bun.BaseModel `bun:"table:saved_view"`
ID string `bun:"id,pk,type:text"`
Data string `bun:"data,type:text"`
}
// specFieldUnmarshalsCleanly reports whether value can be unmarshalled into
// the real type of the given savedviewtypes.SavedViewSpec JSON key.
func specFieldUnmarshalsCleanly(key string, value json.RawMessage) bool {
switch key {
case "displayName", "panelType":
var s string
return json.Unmarshal(value, &s) == nil
case "queries":
var q []qbtypes.QueryEnvelope
return json.Unmarshal(value, &q) == nil
case "selectedFields":
var f []telemetrytypes.TelemetryFieldKey
return json.Unmarshal(value, &f) == nil
case "display":
var d savedviewtypes.Display
return json.Unmarshal(value, &d) == nil
default:
return true
}
}
// specFieldZeroValueJSON is the JSON to substitute for a spec key that fails
// to unmarshal into its real type.
var specFieldZeroValueJSON = map[string]string{
"displayName": `""`,
"panelType": `""`,
"queries": `[]`,
"selectedFields": `[]`,
"display": `{}`,
}
// repairSavedViewData tries to make data unmarshal cleanly into
// savedviewtypes.SavedViewData by blanking, one key at a time, whichever
// top-level spec fields fail to unmarshal into their real type -- e.g. a
// selectedFields shape the 109 migration forwarded verbatim from a
// pre-telemetrytypes.TelemetryFieldKey install, or a queries shape that
// predates the current discriminated-union QueryEnvelope. Every other key is
// left byte-for-byte untouched. Returns ok=false if data/spec aren't even
// JSON objects, or the result still doesn't unmarshal cleanly afterward.
func repairSavedViewData(data string) (fixed string, blanked []string, ok bool) {
var raw map[string]json.RawMessage
if err := json.Unmarshal([]byte(data), &raw); err != nil {
return "", nil, false
}
var spec map[string]json.RawMessage
if err := json.Unmarshal(raw["spec"], &spec); err != nil {
return "", nil, false
}
for key, value := range spec {
if specFieldUnmarshalsCleanly(key, value) {
continue
}
zero, known := specFieldZeroValueJSON[key]
if !known {
continue
}
spec[key] = json.RawMessage(zero)
blanked = append(blanked, key)
}
fixedSpec, err := json.Marshal(spec)
if err != nil {
return "", nil, false
}
raw["spec"] = fixedSpec
fixedData, err := json.Marshal(raw)
if err != nil {
return "", nil, false
}
// verify the fix actually round-trips through the real type before writing it.
if err := json.Unmarshal(fixedData, new(savedviewtypes.SavedViewData)); err != nil {
return "", nil, false
}
return string(fixedData), blanked, true
}
// placeholderSavedViewData is substituted whole when a row can't be repaired
// field-by-field (data/spec aren't JSON objects at all, or repair still
// doesn't unmarshal cleanly). It must itself always unmarshal cleanly, since
// every Get/List reads saved_view.data straight into savedviewtypes.SavedViewData
// -- leaving genuinely-unrepairable data in place would 500 on every future read.
func placeholderSavedViewData(id string) string {
data, err := json.Marshal(savedviewtypes.SavedViewData{
SchemaVersion: savedviewtypes.SavedViewSchemaVersion.StringValue(),
Spec: savedviewtypes.SavedViewSpec{
DisplayName: fmt.Sprintf("corrupted saved view %s", id),
PanelType: savedviewtypes.PanelTypeTable,
},
})
if err != nil {
// marshalling a static, well-formed literal cannot fail.
panic(err)
}
return string(data)
}
func (migration *fixSavedViewSelectedFields) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var rows []*storableSavedViewData
if err := tx.NewSelect().Model(&rows).Scan(ctx); err != nil && err != sql.ErrNoRows {
return err
}
var repaired, replaced int
for _, row := range rows {
// already scans cleanly as-is -- nothing to repair.
if err := json.Unmarshal([]byte(row.Data), new(savedviewtypes.SavedViewData)); err == nil {
continue
}
fixedData, blanked, ok := repairSavedViewData(row.Data)
if !ok {
fixedData = placeholderSavedViewData(row.ID)
replaced++
migration.settings.Logger.WarnContext(ctx, "saved view data could not be repaired field-by-field, replacing with a placeholder view", slog.String("saved_view_id", row.ID))
} else {
repaired++
migration.settings.Logger.WarnContext(ctx, "repaired saved view data by blanking fields that failed to unmarshal", slog.String("saved_view_id", row.ID), slog.Any("fields_blanked", blanked))
}
if _, err := tx.NewUpdate().Model((*storableSavedViewData)(nil)).Set("data = ?", fixedData).Where("id = ?", row.ID).Exec(ctx); err != nil {
return err
}
}
migration.settings.Logger.InfoContext(ctx, "checked saved views for unreadable data", slog.Int("total", len(rows)), slog.Int("repaired", repaired), slog.Int("replaced", replaced))
return tx.Commit()
}
func (migration *fixSavedViewSelectedFields) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -42,22 +42,13 @@ type ClusterRecord struct {
type PostableClusters struct {
Start int64 `json:"start" required:"true"`
End int64 `json:"end" required:"true"`
Filter *ClusterFilter `json:"filter"`
Filter *qbtypes.Filter `json:"filter"`
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
OrderBy *qbtypes.OrderBy `json:"orderBy"`
Offset int `json:"offset"`
Limit int `json:"limit" required:"true"`
}
// ClusterFilter is the attribute filter plus optional secondary filters on the
// derived pod display status(es) (see PodStatus; matches any listed, OR) and node
// readiness (see NodeCondition; matches any listed, OR). Empty FilterByPodStatus / FilterByNodeReadiness = off.
type ClusterFilter struct {
qbtypes.Filter `json:",inline"`
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
FilterByNodeReadiness []NodeCondition `json:"filterByNodeReadiness"`
}
// Validate ensures PostableClusters contains acceptable values.
func (req *PostableClusters) Validate() error {
if req == nil {
@@ -97,19 +88,6 @@ func (req *PostableClusters) Validate() error {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
}
if req.Filter != nil {
for _, s := range req.Filter.FilterByPodStatus {
if !s.IsFilterable() {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
}
}
for _, c := range req.Filter.FilterByNodeReadiness {
if !c.IsFilterable() {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by node readiness: %s", c)
}
}
}
if req.OrderBy != nil {
if !slices.Contains(ClustersValidOrderByKeys, req.OrderBy.Key.Name) {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)

View File

@@ -75,21 +75,13 @@ type ContainerRecord struct {
type PostableContainers struct {
Start int64 `json:"start" required:"true"`
End int64 `json:"end" required:"true"`
Filter *ContainerFilter `json:"filter"`
Filter *qbtypes.Filter `json:"filter"`
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
OrderBy *qbtypes.OrderBy `json:"orderBy"`
Offset int `json:"offset"`
Limit int `json:"limit" required:"true"`
}
// ContainerFilter is the attribute filter plus an optional secondary filter on
// the derived container display status(es) (see ContainerStatus; matches any
// listed, OR). Empty FilterByContainerStatus = off.
type ContainerFilter struct {
qbtypes.Filter `json:",inline"`
FilterByContainerStatus []ContainerStatus `json:"filterByContainerStatus"`
}
// Validate ensures PostableContainers contains acceptable values.
func (req *PostableContainers) Validate() error {
if req == nil {
@@ -129,14 +121,6 @@ func (req *PostableContainers) Validate() error {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
}
if req.Filter != nil {
for _, c := range req.Filter.FilterByContainerStatus {
if !c.IsFilterable() {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by container status: %s", c)
}
}
}
if req.OrderBy != nil {
if !slices.Contains(ContainersValidOrderByKeys, req.OrderBy.Key.Name) {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)

View File

@@ -1,10 +1,6 @@
package inframonitoringtypes
import (
"slices"
"github.com/SigNoz/signoz/pkg/valuer"
)
import "github.com/SigNoz/signoz/pkg/valuer"
// ContainerStatus is the kubectl-style display status of a container, derived
// from k8s.container.status.state (base) + k8s.container.status.reason (overlay).
@@ -35,12 +31,6 @@ var (
ContainerStatusNoData = ContainerStatus{valuer.NewString("no_data")}
)
// IsFilterable reports whether c is a concrete, user-filterable
// container status: any Enum() member except the no_data sentinel.
func (c ContainerStatus) IsFilterable() bool {
return c != ContainerStatusNoData && slices.Contains((ContainerStatus{}).Enum(), any(c))
}
func (ContainerStatus) Enum() []any {
return []any{
ContainerStatusRunning,

View File

@@ -36,20 +36,13 @@ type DaemonSetRecord struct {
type PostableDaemonSets struct {
Start int64 `json:"start" required:"true"`
End int64 `json:"end" required:"true"`
Filter *DaemonSetFilter `json:"filter"`
Filter *qbtypes.Filter `json:"filter"`
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
OrderBy *qbtypes.OrderBy `json:"orderBy"`
Offset int `json:"offset"`
Limit int `json:"limit" required:"true"`
}
// DaemonSetFilter is the attribute filter plus an optional secondary filter on the
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
type DaemonSetFilter struct {
qbtypes.Filter `json:",inline"`
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
}
// Validate ensures PostableDaemonSets contains acceptable values.
func (req *PostableDaemonSets) Validate() error {
if req == nil {
@@ -89,14 +82,6 @@ func (req *PostableDaemonSets) Validate() error {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
}
if req.Filter != nil {
for _, s := range req.Filter.FilterByPodStatus {
if !s.IsFilterable() {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
}
}
}
if req.OrderBy != nil {
if !slices.Contains(DaemonSetsValidOrderByKeys, req.OrderBy.Key.Name) {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)

View File

@@ -34,20 +34,13 @@ type DeploymentRecord struct {
type PostableDeployments struct {
Start int64 `json:"start" required:"true"`
End int64 `json:"end" required:"true"`
Filter *DeploymentFilter `json:"filter"`
Filter *qbtypes.Filter `json:"filter"`
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
OrderBy *qbtypes.OrderBy `json:"orderBy"`
Offset int `json:"offset"`
Limit int `json:"limit" required:"true"`
}
// DeploymentFilter is the attribute filter plus an optional secondary filter on the
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
type DeploymentFilter struct {
qbtypes.Filter `json:",inline"`
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
}
// Validate ensures PostableDeployments contains acceptable values.
func (req *PostableDeployments) Validate() error {
if req == nil {
@@ -87,14 +80,6 @@ func (req *PostableDeployments) Validate() error {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
}
if req.Filter != nil {
for _, s := range req.Filter.FilterByPodStatus {
if !s.IsFilterable() {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
}
}
}
if req.OrderBy != nil {
if !slices.Contains(DeploymentsValidOrderByKeys, req.OrderBy.Key.Name) {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)

View File

@@ -36,20 +36,13 @@ type JobRecord struct {
type PostableJobs struct {
Start int64 `json:"start" required:"true"`
End int64 `json:"end" required:"true"`
Filter *JobFilter `json:"filter"`
Filter *qbtypes.Filter `json:"filter"`
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
OrderBy *qbtypes.OrderBy `json:"orderBy"`
Offset int `json:"offset"`
Limit int `json:"limit" required:"true"`
}
// JobFilter is the attribute filter plus an optional secondary filter on the
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
type JobFilter struct {
qbtypes.Filter `json:",inline"`
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
}
// Validate ensures PostableJobs contains acceptable values.
func (req *PostableJobs) Validate() error {
if req == nil {
@@ -89,14 +82,6 @@ func (req *PostableJobs) Validate() error {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
}
if req.Filter != nil {
for _, s := range req.Filter.FilterByPodStatus {
if !s.IsFilterable() {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
}
}
}
if req.OrderBy != nil {
if !slices.Contains(JobsValidOrderByKeys, req.OrderBy.Key.Name) {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)

View File

@@ -34,20 +34,13 @@ type NamespaceRecord struct {
type PostableNamespaces struct {
Start int64 `json:"start" required:"true"`
End int64 `json:"end" required:"true"`
Filter *NamespaceFilter `json:"filter"`
Filter *qbtypes.Filter `json:"filter"`
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
OrderBy *qbtypes.OrderBy `json:"orderBy"`
Offset int `json:"offset"`
Limit int `json:"limit" required:"true"`
}
// NamespaceFilter is the attribute filter plus an optional secondary filter on the
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
type NamespaceFilter struct {
qbtypes.Filter `json:",inline"`
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
}
// Validate ensures PostableNamespaces contains acceptable values.
func (req *PostableNamespaces) Validate() error {
if req == nil {
@@ -87,14 +80,6 @@ func (req *PostableNamespaces) Validate() error {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
}
if req.Filter != nil {
for _, s := range req.Filter.FilterByPodStatus {
if !s.IsFilterable() {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
}
}
}
if req.OrderBy != nil {
if !slices.Contains(NamespacesValidOrderByKeys, req.OrderBy.Key.Name) {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)

View File

@@ -39,22 +39,13 @@ type NodeRecord struct {
type PostableNodes struct {
Start int64 `json:"start" required:"true"`
End int64 `json:"end" required:"true"`
Filter *NodeFilter `json:"filter"`
Filter *qbtypes.Filter `json:"filter"`
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
OrderBy *qbtypes.OrderBy `json:"orderBy"`
Offset int `json:"offset"`
Limit int `json:"limit" required:"true"`
}
// NodeFilter is the attribute filter plus an optional secondary filter on the
// derived pod display status(es) (see PodStatus; matches any listed, OR) and node
// readiness (see NodeCondition; matches any listed, OR). Empty FilterByPodStatus / FilterByNodeReadiness = off.
type NodeFilter struct {
qbtypes.Filter `json:",inline"`
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
FilterByNodeReadiness []NodeCondition `json:"filterByNodeReadiness"`
}
// Validate ensures PostableNodes contains acceptable values.
func (req *PostableNodes) Validate() error {
if req == nil {
@@ -94,19 +85,6 @@ func (req *PostableNodes) Validate() error {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
}
if req.Filter != nil {
for _, s := range req.Filter.FilterByPodStatus {
if !s.IsFilterable() {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
}
}
for _, c := range req.Filter.FilterByNodeReadiness {
if !c.IsFilterable() {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by node readiness: %s", c)
}
}
}
if req.OrderBy != nil {
if !slices.Contains(NodesValidOrderByKeys, req.OrderBy.Key.Name) {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)

View File

@@ -1,10 +1,6 @@
package inframonitoringtypes
import (
"slices"
"github.com/SigNoz/signoz/pkg/valuer"
)
import "github.com/SigNoz/signoz/pkg/valuer"
type NodeCondition struct {
valuer.String
@@ -24,12 +20,6 @@ func (NodeCondition) Enum() []any {
}
}
// IsFilterable reports whether c is a concrete, user-filterable
// node readiness: any Enum() member except the no_data sentinel.
func (c NodeCondition) IsFilterable() bool {
return c != NodeConditionNoData && slices.Contains((NodeCondition{}).Enum(), any(c))
}
// Numeric values emitted by the k8s.node.condition_ready metric
// (source: OTel kubeletstats receiver).
const (

View File

@@ -63,20 +63,13 @@ type PodRecord struct {
type PostablePods struct {
Start int64 `json:"start" required:"true"`
End int64 `json:"end" required:"true"`
Filter *PodFilter `json:"filter"`
Filter *qbtypes.Filter `json:"filter"`
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
OrderBy *qbtypes.OrderBy `json:"orderBy"`
Offset int `json:"offset"`
Limit int `json:"limit" required:"true"`
}
// PodFilter is the attribute filter plus an optional secondary filter on the
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
type PodFilter struct {
qbtypes.Filter `json:",inline"`
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
}
// Validate ensures PostablePods contains acceptable values.
func (req *PostablePods) Validate() error {
if req == nil {
@@ -116,14 +109,6 @@ func (req *PostablePods) Validate() error {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
}
if req.Filter != nil {
for _, s := range req.Filter.FilterByPodStatus {
if !s.IsFilterable() {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
}
}
}
if req.OrderBy != nil {
if !slices.Contains(PodsValidOrderByKeys, req.OrderBy.Key.Name) {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)

View File

@@ -1,10 +1,6 @@
package inframonitoringtypes
import (
"slices"
"github.com/SigNoz/signoz/pkg/valuer"
)
import "github.com/SigNoz/signoz/pkg/valuer"
// PodStatus is the kubectl-style pod display status, derived from
// k8s.pod.phase + k8s.pod.status_reason + k8s.container.status.reason
@@ -70,12 +66,6 @@ func (PodStatus) Enum() []any {
}
}
// IsFilterable reports whether s is a concrete, user-filterable pod
// status: any Enum() member except the no_data sentinel.
func (s PodStatus) IsFilterable() bool {
return s != PodStatusNoData && slices.Contains((PodStatus{}).Enum(), any(s))
}
const PodNameAttrKey = "k8s.pod.name"
const (

View File

@@ -34,20 +34,13 @@ type StatefulSetRecord struct {
type PostableStatefulSets struct {
Start int64 `json:"start" required:"true"`
End int64 `json:"end" required:"true"`
Filter *StatefulSetFilter `json:"filter"`
Filter *qbtypes.Filter `json:"filter"`
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
OrderBy *qbtypes.OrderBy `json:"orderBy"`
Offset int `json:"offset"`
Limit int `json:"limit" required:"true"`
}
// StatefulSetFilter is the attribute filter plus an optional secondary filter on the
// derived pod display status(es) (see PodStatus); matches any listed (OR). Empty = off.
type StatefulSetFilter struct {
qbtypes.Filter `json:",inline"`
FilterByPodStatus []PodStatus `json:"filterByPodStatus"`
}
// Validate ensures PostableStatefulSets contains acceptable values.
func (req *PostableStatefulSets) Validate() error {
if req == nil {
@@ -87,14 +80,6 @@ func (req *PostableStatefulSets) Validate() error {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
}
if req.Filter != nil {
for _, s := range req.Filter.FilterByPodStatus {
if !s.IsFilterable() {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid filter by pod status: %s", s)
}
}
}
if req.OrderBy != nil {
if !slices.Contains(StatefulSetsValidOrderByKeys, req.OrderBy.Key.Name) {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)

View File

@@ -27,75 +27,28 @@ var (
SourceMeter = Source{valuer.NewString("meter")}
)
// SavedView is the domain/wire shape -- schemaVersion and spec are promoted
// to the top level, matching dashboardtypes.DashboardV2 and ruletypes'
// v2alpha1 rules. StorableSavedView is the distinct bun-mapped row shape;
// the two diverge because bun maps Data to a single opaque `data` column,
// which is incompatible with promoting its fields to the top level for JSON.
type SavedView struct {
types.Identifiable
types.TimeAuditable
types.UserAuditable
OrgID string `json:"-"`
Name string `json:"name"`
Source Source `json:"source"`
SchemaVersion SchemaVersion `json:"schemaVersion" required:"true"`
Spec SavedViewSpec `json:"spec" required:"true"`
}
// StorableSavedView is the row shape bun maps to the saved_view table.
type StorableSavedView struct {
bun.BaseModel `bun:"table:saved_view"`
types.Identifiable
types.TimeAuditable
types.UserAuditable
OrgID string `bun:"org_id,notnull"`
Name string `bun:"name,type:text,notnull"`
Source Source `bun:"source,type:text,notnull"`
Data SavedViewData `bun:"data,type:text,notnull"`
}
func (s *StorableSavedView) ToSavedView() *SavedView {
return &SavedView{
Identifiable: s.Identifiable,
TimeAuditable: s.TimeAuditable,
UserAuditable: s.UserAuditable,
OrgID: s.OrgID,
Name: s.Name,
Source: s.Source,
SchemaVersion: SchemaVersion{valuer.NewString(s.Data.SchemaVersion)},
Spec: s.Data.Spec,
}
}
func NewStorableSavedView(view *SavedView) *StorableSavedView {
return &StorableSavedView{
Identifiable: view.Identifiable,
TimeAuditable: view.TimeAuditable,
UserAuditable: view.UserAuditable,
OrgID: view.OrgID,
Name: view.Name,
Source: view.Source,
Data: SavedViewData{
SchemaVersion: view.SchemaVersion.StringValue(),
Spec: view.Spec,
},
}
OrgID string `json:"-" bun:"org_id,notnull"`
Name string `json:"name" bun:"name,type:text,notnull"`
Source Source `json:"source" bun:"source,type:text,notnull"`
Data SavedViewData `json:"data" bun:"data,type:text,notnull"`
}
type PostableSavedView struct {
Name string `json:"name"`
GenerateName bool `json:"generateName"`
Source Source `json:"source" required:"true"`
SchemaVersion SchemaVersion `json:"schemaVersion" required:"true"`
Spec SavedViewSpec `json:"spec" required:"true"`
Name string `json:"name"`
GenerateName bool `json:"generateName"`
Source Source `json:"source" required:"true"`
Data SavedViewData `json:"data" required:"true"`
}
type UpdatableSavedView struct {
Source Source `json:"source" required:"true"`
SchemaVersion SchemaVersion `json:"schemaVersion" required:"true"`
Spec SavedViewSpec `json:"spec" required:"true"`
Source Source `json:"source" required:"true"`
Data SavedViewData `json:"data" required:"true"`
}
type ListSavedViewsParams struct {
@@ -130,7 +83,7 @@ func (postable PostableSavedView) ToSavedView(orgID string, createdBy string) *S
name := postable.Name
if postable.GenerateName {
name = generateSavedViewName(postable.Spec.DisplayName)
name = generateSavedViewName(postable.Data.Spec.DisplayName)
}
return &SavedView{
@@ -140,8 +93,7 @@ func (postable PostableSavedView) ToSavedView(orgID string, createdBy string) *S
OrgID: orgID,
Name: name,
Source: postable.Source,
SchemaVersion: postable.SchemaVersion,
Spec: postable.Spec,
Data: postable.Data,
}
}
@@ -154,8 +106,7 @@ func (updatable UpdatableSavedView) ToSavedView(id valuer.UUID, orgID string, up
UserAuditable: types.UserAuditable{UpdatedBy: updatedBy},
OrgID: orgID,
Source: updatable.Source,
SchemaVersion: updatable.SchemaVersion,
Spec: updatable.Spec,
Data: updatable.Data,
}
}
@@ -166,11 +117,8 @@ func (p *PostableSavedView) Validate() error {
if err := p.Source.Validate(); err != nil {
return err
}
if err := p.SchemaVersion.Validate(); err != nil {
return err
}
return p.Spec.Validate()
return p.Data.Validate()
}
func (p *PostableSavedView) validateName() error {
@@ -187,11 +135,8 @@ func (u *UpdatableSavedView) Validate() error {
if err := u.Source.Validate(); err != nil {
return err
}
if err := u.SchemaVersion.Validate(); err != nil {
return err
}
return u.Spec.Validate()
return u.Data.Validate()
}
func (p *ListSavedViewsParams) Validate() error {

View File

@@ -11,18 +11,22 @@ import (
func validPostableSavedView() PostableSavedView {
return PostableSavedView{
Name: "my-view",
Source: SourceLogs,
SchemaVersion: SavedViewSchemaVersion,
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
Name: "my-view",
Source: SourceLogs,
Data: SavedViewData{
SchemaVersion: SavedViewSchemaVersion,
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
},
}
}
func validUpdatableSavedView() UpdatableSavedView {
return UpdatableSavedView{
Source: SourceLogs,
SchemaVersion: SavedViewSchemaVersion,
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
Source: SourceLogs,
Data: SavedViewData{
SchemaVersion: SavedViewSchemaVersion,
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
},
}
}
@@ -65,7 +69,7 @@ func TestPostableSavedViewValidate(t *testing.T) {
t.Run("invalid saved view data is rejected", func(t *testing.T) {
view := validPostableSavedView()
view.SchemaVersion = SchemaVersion{valuer.NewString("v1")}
view.Data.SchemaVersion = "v1"
assert.Error(t, view.Validate())
})
@@ -96,7 +100,7 @@ func TestPostableSavedViewValidate(t *testing.T) {
t.Run("empty displayName is rejected", func(t *testing.T) {
view := validPostableSavedView()
view.Spec.DisplayName = ""
view.Data.Spec.DisplayName = ""
assert.ErrorContains(t, view.Validate(), "displayName is required")
})
}
@@ -115,7 +119,7 @@ func TestUpdatableSavedViewValidate(t *testing.T) {
t.Run("empty displayName is rejected", func(t *testing.T) {
view := validUpdatableSavedView()
view.Spec.DisplayName = ""
view.Data.Spec.DisplayName = ""
assert.ErrorContains(t, view.Validate(), "displayName is required")
})
}
@@ -149,8 +153,7 @@ func TestNewSavedView(t *testing.T) {
assert.Equal(t, "creator@signoz.io", savedView.UpdatedBy)
assert.Equal(t, view.Name, savedView.Name)
assert.Equal(t, view.Source, savedView.Source)
assert.Equal(t, view.SchemaVersion, savedView.SchemaVersion)
assert.Equal(t, view.Spec, savedView.Spec)
assert.Equal(t, view.Data, savedView.Data)
assert.False(t, savedView.CreatedAt.IsZero())
assert.Equal(t, savedView.CreatedAt, savedView.UpdatedAt)
}
@@ -160,14 +163,14 @@ func TestNewSavedView_GeneratesNameWhenEmpty(t *testing.T) {
view := validPostableSavedView()
view.Name = ""
view.GenerateName = true
view.Spec.DisplayName = "My View!"
view.Data.Spec.DisplayName = "My View!"
savedView := view.ToSavedView(orgID, "creator@signoz.io")
assert.NotEmpty(t, savedView.Name)
assert.Empty(t, validation.IsDNS1123Label(savedView.Name), "generated name must be a valid DNS-1123 label")
assert.True(t, strings.HasPrefix(savedView.Name, "my-view-"))
assert.Equal(t, "My View!", savedView.Spec.DisplayName)
assert.Equal(t, "My View!", savedView.Data.Spec.DisplayName)
}
func TestGenerateSavedViewName(t *testing.T) {

View File

@@ -28,7 +28,7 @@ func (t *StoreTest) Store() savedviewtypes.Store { return t.store }
func (t *StoreTest) Mock() sqlmock.Sqlmock { return t.mock }
func savedViewRow(view *savedviewtypes.SavedView) []driver.Value {
data, _ := json.Marshal(savedviewtypes.NewStorableSavedView(view).Data)
data, _ := json.Marshal(view.Data)
return []driver.Value{
view.ID.StringValue(),
view.CreatedAt,

View File

@@ -8,7 +8,7 @@ import (
)
// SavedViewSchemaVersion is the only schemaVersion currently.
var SavedViewSchemaVersion = SchemaVersion{valuer.NewString("v2")}
const SavedViewSchemaVersion = "v2"
var (
PanelTypeValue = PanelType{valuer.NewString("value")}
@@ -30,9 +30,9 @@ type Display struct {
type SavedViewSpec struct {
DisplayName string `json:"displayName" required:"true"`
PanelType PanelType `json:"panelType" required:"true"`
Queries []qbtypes.QueryEnvelope `json:"queries" required:"true" nullable:"false" minItems:"1"`
SelectedFields []telemetrytypes.TelemetryFieldKey `json:"selectedFields" nullable:"false"`
Display Display `json:"display"`
Queries []qbtypes.QueryEnvelope `json:"queries" required:"true" nullable:"false"`
SelectedFields []telemetrytypes.TelemetryFieldKey `json:"selectedFields" required:"true" nullable:"false"`
Display Display `json:"display" required:"true"`
}
// SavedViewData is what's persisted as saved view data.
@@ -41,11 +41,6 @@ type SavedViewData struct {
Spec SavedViewSpec `json:"spec" required:"true"`
}
// SchemaVersion has v2 as the only value currently.
type SchemaVersion struct {
valuer.String
}
// PanelType is the explore-page panel a saved view renders as.
type PanelType struct {
valuer.String
@@ -70,17 +65,6 @@ func (p PanelType) Validate() error {
}
}
func (SchemaVersion) Enum() []any {
return []any{SavedViewSchemaVersion}
}
func (s SchemaVersion) Validate() error {
if s != SavedViewSchemaVersion {
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "schemaVersion must be %q, got %q", SavedViewSchemaVersion.StringValue(), s.StringValue())
}
return nil
}
func (s *SavedViewSpec) Validate() error {
if s.DisplayName == "" {
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "displayName is required")
@@ -91,3 +75,11 @@ func (s *SavedViewSpec) Validate() error {
return (&qbtypes.CompositeQuery{Queries: s.Queries}).Validate()
}
func (d *SavedViewData) Validate() error {
if d.SchemaVersion != SavedViewSchemaVersion {
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "schemaVersion must be %q, got %q", SavedViewSchemaVersion, d.SchemaVersion)
}
return d.Spec.Validate()
}

View File

@@ -1,15 +1,12 @@
package savedviewtypes
import (
"encoding/json"
"testing"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func validQueries() []qbtypes.QueryEnvelope {
@@ -78,7 +75,7 @@ func TestSavedViewSpecValidate(t *testing.T) {
expectError: true,
},
{
name: "selectedFields and display populated is still valid",
name: "selected fields and display are not required",
spec: SavedViewSpec{
DisplayName: "My View",
PanelType: PanelTypeTable,
@@ -88,36 +85,6 @@ func TestSavedViewSpecValidate(t *testing.T) {
},
expectError: false,
},
{
name: "nil selectedFields is valid -- neither field is actually required",
spec: SavedViewSpec{
DisplayName: "My View",
PanelType: PanelTypeTable,
Queries: validQueries(),
SelectedFields: nil,
},
expectError: false,
},
{
name: "empty (non-nil) selectedFields is valid",
spec: SavedViewSpec{
DisplayName: "My View",
PanelType: PanelTypeTable,
Queries: validQueries(),
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
},
expectError: false,
},
{
name: "zero-value display is valid",
spec: SavedViewSpec{
DisplayName: "My View",
PanelType: PanelTypeTable,
Queries: validQueries(),
Display: Display{},
},
expectError: false,
},
}
for _, c := range cases {
@@ -132,44 +99,37 @@ func TestSavedViewSpecValidate(t *testing.T) {
}
}
func TestSavedViewSpecJSONUnmarshal_OptionalFields(t *testing.T) {
base := `"displayName":"My View","panelType":"table","queries":[{"type":"builder_query","spec":{"signal":"logs","aggregations":[{"expression":"count()"}]}}]`
func TestSavedViewDataValidate(t *testing.T) {
cases := []struct {
name string
json string
name string
data SavedViewData
expectError bool
}{
{name: "selectedFields and display omitted entirely", json: `{` + base + `}`},
{name: "selectedFields and display explicitly null", json: `{` + base + `,"selectedFields":null,"display":null}`},
{name: "selectedFields empty array, display empty object", json: `{` + base + `,"selectedFields":[],"display":{}}`},
{
name: "valid data",
data: SavedViewData{SchemaVersion: SavedViewSchemaVersion, Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()}},
expectError: false,
},
{
name: "wrong schema version is rejected",
data: SavedViewData{SchemaVersion: "v1", Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()}},
expectError: true,
},
{
name: "empty schema version is rejected",
data: SavedViewData{Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()}},
expectError: true,
},
{
name: "invalid spec is rejected",
data: SavedViewData{SchemaVersion: SavedViewSchemaVersion, Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph}},
expectError: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var spec SavedViewSpec
err := json.Unmarshal([]byte(c.json), &spec)
require.NoError(t, err)
assert.NoError(t, spec.Validate())
assert.Empty(t, spec.SelectedFields)
assert.Equal(t, Display{}, spec.Display)
})
}
}
func TestSchemaVersionValidate(t *testing.T) {
cases := []struct {
name string
schemaVersion SchemaVersion
expectError bool
}{
{name: "valid schema version", schemaVersion: SavedViewSchemaVersion, expectError: false},
{name: "wrong schema version is rejected", schemaVersion: SchemaVersion{valuer.NewString("v1")}, expectError: true},
{name: "empty schema version is rejected", schemaVersion: SchemaVersion{}, expectError: true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
err := c.schemaVersion.Validate()
err := c.data.Validate()
if c.expectError {
assert.Error(t, err)
} else {

View File

@@ -1,721 +0,0 @@
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"go/format"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
const (
kindAttribute = "attribute"
kindMetric = "metric"
)
type stringListFlag []string
func (f *stringListFlag) String() string { return strings.Join(*f, ",") }
func (f *stringListFlag) Set(value string) error {
*f = append(*f, value)
return nil
}
type schemaFile struct {
FileFormat string `yaml:"file_format"`
SchemaURL string `yaml:"schema_url"`
Versions map[string]schemaVersion `yaml:"versions"`
}
type schemaVersion struct {
All changeSection `yaml:"all"`
Resources changeSection `yaml:"resources"`
Spans changeSection `yaml:"spans"`
Logs changeSection `yaml:"logs"`
Metrics changeSection `yaml:"metrics"`
}
type changeSection struct {
Changes []schemaChange `yaml:"changes"`
}
type schemaChange struct {
RenameAttributes *attributeRename `yaml:"rename_attributes"`
RenameMetrics map[string]string `yaml:"rename_metrics"`
}
type attributeRename struct {
AttributeMap map[string]string `yaml:"attribute_map"`
ApplyToMetrics []string `yaml:"apply_to_metrics"`
}
type overlayFile struct {
DefaultEnabled bool `yaml:"default_enabled"`
// Families is keyed only by current name. One name cannot carry separate
// policies for attribute and metric families; set kind explicitly whenever
// a metric-name family is configured.
Families map[string]overlayFamily `yaml:"families"`
}
type overlayFamily struct {
Enabled *bool `yaml:"enabled"`
Kind string `yaml:"kind"`
Old []string `yaml:"old"`
AddOld []string `yaml:"add_old"`
ExcludeOld []string `yaml:"exclude_old"`
Contexts []string `yaml:"contexts"`
Signals []string `yaml:"signals"`
AddContexts []string `yaml:"add_contexts"`
AddSignals []string `yaml:"add_signals"`
ApplyToMetrics []string `yaml:"apply_to_metrics"`
AddApplyToMetrics []string `yaml:"add_apply_to_metrics"`
ValueMap map[string]string `yaml:"value_map"`
}
type edge struct {
old string
current string
kind string
contexts []string
signals []string
allContexts bool
allSignals bool
applyToMetrics []string
}
type graphKey struct{ kind, name string }
type generatedFamily struct {
Current string
Old []string
Kind string
Contexts []string
Signals []string
ApplyToMetrics []string
ValueMap map[string]string
}
func main() {
root, err := findRepoRoot()
if err != nil {
fatal(err)
}
var schemaPaths stringListFlag
flag.Var(&schemaPaths, "schema", "schema source (repeatable)")
overlayPath := flag.String("overlay", filepath.Join(root, "scripts/semconv/overlay.yaml"), "SigNoz overlay")
goOutput := flag.String("go-out", filepath.Join(root, "pkg/semconv/families_gen.go"), "generated Go output")
tsOutput := flag.String("ts-out", filepath.Join(root, "frontend/src/constants/generated/semconvFamilies.gen.ts"), "generated TypeScript output")
check := flag.Bool("check", false, "fail if generated files are stale")
flag.Parse()
if len(schemaPaths) == 0 {
schemaPaths = append(schemaPaths, filepath.Join(root, "scripts/semconv/schema-1.42.0.yaml"))
}
families, err := generate(schemaPaths, *overlayPath)
if err != nil {
fatal(err)
}
goBytes, err := renderGo(families)
if err != nil {
fatal(err)
}
tsBytes := renderTypeScript(families)
if *check {
if err := checkFile(*goOutput, goBytes); err != nil {
fatal(err)
}
if err := checkFile(*tsOutput, tsBytes); err != nil {
fatal(err)
}
return
}
if err := os.WriteFile(*goOutput, goBytes, 0o644); err != nil {
fatal(err)
}
if err := os.WriteFile(*tsOutput, tsBytes, 0o644); err != nil {
fatal(err)
}
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
func findRepoRoot() (string, error) {
dir, err := os.Getwd()
if err != nil {
return "", err
}
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
return "", errors.New("could not find repository root")
}
dir = parent
}
}
func generate(schemaPaths []string, overlayPath string) ([]generatedFamily, error) {
var schemas []schemaFile
for _, path := range schemaPaths {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read schema %s: %w", path, err)
}
var schema schemaFile
if err := decodeKnownFields(data, &schema); err != nil {
return nil, fmt.Errorf("parse schema %s: %w", path, err)
}
schemas = append(schemas, schema)
}
overlayData, err := os.ReadFile(overlayPath)
if err != nil {
return nil, fmt.Errorf("read overlay: %w", err)
}
var overlay overlayFile
if err := decodeKnownFields(overlayData, &overlay); err != nil {
return nil, fmt.Errorf("parse overlay: %w", err)
}
return buildFamilies(schemas, overlay)
}
func decodeKnownFields(data []byte, target any) error {
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)
return decoder.Decode(target)
}
func collectEdges(schemas []schemaFile) ([]edge, error) {
var edges []edge
for _, schema := range schemas {
versions := make([]string, 0, len(schema.Versions))
versionParts := make(map[string][3]int, len(schema.Versions))
for version := range schema.Versions {
parts, err := parseSchemaVersion(version)
if err != nil {
return nil, err
}
versions = append(versions, version)
versionParts[version] = parts
}
sort.Slice(versions, func(i, j int) bool {
return compareVersionParts(versionParts[versions[i]], versionParts[versions[j]]) < 0
})
for _, versionName := range versions {
version := schema.Versions[versionName]
var versionEdges []edge
sections := []struct {
name string
section changeSection
}{
{name: "all", section: version.All},
{name: "resources", section: version.Resources},
{name: "spans", section: version.Spans},
{name: "logs", section: version.Logs},
{name: "metrics", section: version.Metrics},
}
for _, scoped := range sections {
contexts, signals, allContexts, allSignals, err := scopeForSection(scoped.name)
if err != nil {
return nil, err
}
for _, change := range scoped.section.Changes {
if change.RenameAttributes != nil {
for _, old := range sortedMapKeys(change.RenameAttributes.AttributeMap) {
versionEdges = append(versionEdges, edge{
old: old, current: change.RenameAttributes.AttributeMap[old], kind: kindAttribute,
contexts: contexts, signals: signals,
allContexts: allContexts, allSignals: allSignals,
applyToMetrics: change.RenameAttributes.ApplyToMetrics,
})
}
}
for _, old := range sortedMapKeys(change.RenameMetrics) {
versionEdges = append(versionEdges, edge{
old: old, current: change.RenameMetrics[old], kind: kindMetric,
contexts: []string{"metric"}, signals: []string{"metrics"},
})
}
}
}
if err := rejectSameVersionChains(versionName, versionEdges); err != nil {
return nil, err
}
edges = append(edges, versionEdges...)
}
}
return edges, nil
}
func rejectSameVersionChains(version string, edges []edge) error {
oldNames := make(map[graphKey]struct{}, len(edges))
for _, item := range edges {
oldNames[graphKey{kind: item.kind, name: item.old}] = struct{}{}
}
for _, item := range edges {
if _, ok := oldNames[graphKey{kind: item.kind, name: item.current}]; ok {
return fmt.Errorf(
"schema version %q contains a same-version %s rename chain through %q",
version,
item.kind,
item.current,
)
}
}
return nil
}
func parseSchemaVersion(version string) ([3]int, error) {
parts := strings.Split(version, ".")
if len(parts) != 3 {
return [3]int{}, fmt.Errorf("schema version %q must contain major, minor, and patch numbers", version)
}
var parsed [3]int
for i, part := range parts {
value, err := strconv.Atoi(part)
if err != nil || value < 0 {
return [3]int{}, fmt.Errorf("schema version %q contains invalid numeric component %q", version, part)
}
parsed[i] = value
}
return parsed, nil
}
func compareVersionParts(left, right [3]int) int {
for i := range left {
if left[i] < right[i] {
return -1
}
if left[i] > right[i] {
return 1
}
}
return 0
}
func scopeForSection(section string) (contexts, signals []string, allContexts, allSignals bool, err error) {
switch section {
case "all":
return nil, nil, true, true, nil
case "resources":
return []string{"resource"}, nil, false, true, nil
case "spans":
return []string{"attribute"}, []string{"traces"}, false, false, nil
case "logs":
return []string{"attribute"}, []string{"logs"}, false, false, nil
case "metrics":
return []string{"attribute"}, []string{"metrics"}, false, false, nil
default:
return nil, nil, false, false, fmt.Errorf("unsupported schema section %q", section)
}
}
func buildFamilies(schemas []schemaFile, overlay overlayFile) ([]generatedFamily, error) {
edges, err := collectEdges(schemas)
if err != nil {
return nil, err
}
next := make(map[graphKey]string)
for _, item := range edges {
key := graphKey{kind: item.kind, name: item.old}
if existing, ok := next[key]; ok && existing == item.current {
// Repeated entries are common in chained schema histories. Treat an
// identical edge as a no-op so it cannot sever a later edge in the
// same chain (A -> B, B -> C, then a repeated A -> B).
continue
}
// Schema history occasionally repeats an old name with a newer direct
// destination or rolls a rename back. Edges are collected
// oldest-to-newest, so the latest published current name must be a root.
delete(next, graphKey{kind: item.kind, name: item.current})
next[key] = item.current
}
type familyState struct {
family generatedFamily
distance map[string]int
allContexts bool
allSignals bool
}
states := map[graphKey]*familyState{}
for _, item := range edges {
root, distance, err := rootFor(next, item.kind, item.old)
if err != nil {
return nil, err
}
key := graphKey{kind: item.kind, name: root}
state := states[key]
if state == nil {
state = &familyState{
family: generatedFamily{Current: root, Kind: item.kind},
distance: map[string]int{},
}
states[key] = state
}
if prior, ok := state.distance[item.old]; !ok || distance < prior {
state.distance[item.old] = distance
}
state.allContexts = state.allContexts || item.allContexts
state.allSignals = state.allSignals || item.allSignals
state.family.Contexts = appendUnique(state.family.Contexts, item.contexts...)
state.family.Signals = appendUnique(state.family.Signals, item.signals...)
state.family.ApplyToMetrics = appendUnique(state.family.ApplyToMetrics, item.applyToMetrics...)
}
for _, state := range states {
for old := range state.distance {
if old != state.family.Current {
state.family.Old = append(state.family.Old, old)
}
}
sort.Slice(state.family.Old, func(i, j int) bool {
left, right := state.family.Old[i], state.family.Old[j]
if state.distance[left] != state.distance[right] {
return state.distance[left] < state.distance[right]
}
return left < right
})
if state.allContexts {
state.family.Contexts = nil
} else {
sort.Strings(state.family.Contexts)
}
if state.allSignals {
state.family.Signals = nil
} else {
sort.Strings(state.family.Signals)
}
sort.Strings(state.family.ApplyToMetrics)
}
for _, current := range sortedMapKeys(overlay.Families) {
policy := overlay.Families[current]
kind, err := normalizedOverlayKind(current, policy)
if err != nil {
return nil, err
}
policy.Kind = kind
overlay.Families[current] = policy
key := graphKey{kind: kind, name: current}
state := states[key]
if state == nil {
if len(policy.Old) == 0 {
return nil, fmt.Errorf(
"overlay family %q with kind %q is absent from schemas and has no old members",
current,
kind,
)
}
state = &familyState{
family: generatedFamily{Current: current, Kind: kind, Old: append([]string(nil), policy.Old...)},
distance: map[string]int{},
}
states[key] = state
}
applyOverlay(&state.family, policy)
}
var result []generatedFamily
for key, state := range states {
policy, hasPolicy := overlay.Families[key.name]
enabled := overlay.DefaultEnabled
if hasPolicy && policy.Kind != key.kind {
hasPolicy = false
}
if hasPolicy && policy.Enabled != nil {
enabled = *policy.Enabled
}
if !enabled {
continue
}
if len(state.family.Old) == 0 {
return nil, fmt.Errorf(
"enabled family %q with kind %q has no old members",
state.family.Current,
state.family.Kind,
)
}
sort.Strings(state.family.Contexts)
sort.Strings(state.family.Signals)
sort.Strings(state.family.ApplyToMetrics)
result = append(result, state.family)
}
sort.Slice(result, func(i, j int) bool {
if result[i].Current != result[j].Current {
return result[i].Current < result[j].Current
}
return result[i].Kind < result[j].Kind
})
return result, nil
}
func rootFor(next map[graphKey]string, kind, name string) (string, int, error) {
seen := map[string]bool{}
distance := 0
for {
if seen[name] {
return "", 0, fmt.Errorf("rename cycle for %s %q", kind, name)
}
seen[name] = true
current, ok := next[graphKey{kind: kind, name: name}]
if !ok {
return name, distance, nil
}
name = current
distance++
}
}
func normalizedOverlayKind(current string, policy overlayFamily) (string, error) {
kind := policy.Kind
if kind == "" {
kind = kindAttribute
}
if kind != kindAttribute && kind != kindMetric {
return "", fmt.Errorf("overlay family %q has unsupported kind %q", current, kind)
}
return kind, nil
}
func applyOverlay(family *generatedFamily, policy overlayFamily) {
if policy.Kind != "" {
family.Kind = policy.Kind
}
if policy.Old != nil {
family.Old = append([]string(nil), policy.Old...)
}
family.Old = appendUnique(family.Old, policy.AddOld...)
if len(policy.ExcludeOld) > 0 {
excluded := make(map[string]bool, len(policy.ExcludeOld))
for _, old := range policy.ExcludeOld {
excluded[old] = true
}
family.Old = deleteMatching(family.Old, excluded)
}
if policy.Contexts != nil {
family.Contexts = append([]string(nil), policy.Contexts...)
}
if policy.Signals != nil {
family.Signals = append([]string(nil), policy.Signals...)
}
family.Contexts = appendUnique(family.Contexts, policy.AddContexts...)
family.Signals = appendUnique(family.Signals, policy.AddSignals...)
if policy.ApplyToMetrics != nil {
family.ApplyToMetrics = append([]string(nil), policy.ApplyToMetrics...)
}
family.ApplyToMetrics = appendUnique(family.ApplyToMetrics, policy.AddApplyToMetrics...)
if policy.ValueMap != nil {
family.ValueMap = make(map[string]string, len(policy.ValueMap))
for old, current := range policy.ValueMap {
family.ValueMap[old] = current
}
}
}
func appendUnique(values []string, additions ...string) []string {
seen := make(map[string]bool, len(values)+len(additions))
for _, value := range values {
seen[value] = true
}
for _, value := range additions {
if value == "" || seen[value] {
continue
}
seen[value] = true
values = append(values, value)
}
return values
}
func deleteMatching(values []string, excluded map[string]bool) []string {
result := values[:0]
for _, value := range values {
if !excluded[value] {
result = append(result, value)
}
}
return result
}
func renderGo(families []generatedFamily) ([]byte, error) {
var out bytes.Buffer
out.WriteString("// Code generated by scripts/semconv. DO NOT EDIT.\n\n")
out.WriteString("package semconv\n\n")
needsTelemetryTypes := false
for _, family := range families {
if len(family.Contexts) > 0 || len(family.Signals) > 0 {
needsTelemetryTypes = true
break
}
}
if needsTelemetryTypes {
out.WriteString("import \"github.com/SigNoz/signoz/pkg/types/telemetrytypes\"\n\n")
}
out.WriteString("var families = []Family{\n")
for _, family := range families {
contexts, err := goFieldContextSlice(family.Contexts)
if err != nil {
return nil, fmt.Errorf("render family %q: %w", family.Current, err)
}
signals, err := goSignalSlice(family.Signals)
if err != nil {
return nil, fmt.Errorf("render family %q: %w", family.Current, err)
}
out.WriteString("\t{\n")
fmt.Fprintf(&out, "\t\tCurrent: %s,\n", strconv.Quote(family.Current))
fmt.Fprintf(&out, "\t\tOld: %s,\n", goStringSlice(family.Old))
if family.Kind == kindMetric {
out.WriteString("\t\tKind: KindMetric,\n")
} else {
out.WriteString("\t\tKind: KindAttribute,\n")
}
fmt.Fprintf(&out, "\t\tContexts: %s,\n", contexts)
fmt.Fprintf(&out, "\t\tSignals: %s,\n", signals)
fmt.Fprintf(&out, "\t\tApplyToMetrics: %s,\n", goStringSlice(family.ApplyToMetrics))
if len(family.ValueMap) > 0 {
out.WriteString("\t\tValueMap: map[string]string{\n")
keys := sortedMapKeys(family.ValueMap)
for _, key := range keys {
fmt.Fprintf(&out, "\t\t\t%s: %s,\n", strconv.Quote(key), strconv.Quote(family.ValueMap[key]))
}
out.WriteString("\t\t},\n")
}
out.WriteString("\t},\n")
}
out.WriteString("}\n")
return format.Source(out.Bytes())
}
func goStringSlice(values []string) string {
if len(values) == 0 {
return "nil"
}
quoted := make([]string, len(values))
for i, value := range values {
quoted[i] = strconv.Quote(value)
}
return "[]string{" + strings.Join(quoted, ", ") + "}"
}
func goFieldContextSlice(values []string) (string, error) {
if len(values) == 0 {
return "nil", nil
}
constants := make([]string, len(values))
for i, value := range values {
switch value {
case "metric":
constants[i] = "telemetrytypes.FieldContextMetric"
case "resource":
constants[i] = "telemetrytypes.FieldContextResource"
case "attribute":
constants[i] = "telemetrytypes.FieldContextAttribute"
default:
return "", fmt.Errorf("unsupported field context %q", value)
}
}
return "[]telemetrytypes.FieldContext{" + strings.Join(constants, ", ") + "}", nil
}
func goSignalSlice(values []string) (string, error) {
if len(values) == 0 {
return "nil", nil
}
constants := make([]string, len(values))
for i, value := range values {
switch value {
case "traces":
constants[i] = "telemetrytypes.SignalTraces"
case "logs":
constants[i] = "telemetrytypes.SignalLogs"
case "metrics":
constants[i] = "telemetrytypes.SignalMetrics"
default:
return "", fmt.Errorf("unsupported signal %q", value)
}
}
return "[]telemetrytypes.Signal{" + strings.Join(constants, ", ") + "}", nil
}
func renderTypeScript(families []generatedFamily) []byte {
var out bytes.Buffer
out.WriteString("// Code generated by scripts/semconv. DO NOT EDIT.\n\n")
out.WriteString("export type SemconvFamily = {\n")
out.WriteString("\treadonly current: string;\n\treadonly old: readonly string[];\n")
out.WriteString("\treadonly kind: 'attribute' | 'metric';\n")
out.WriteString("\treadonly contexts: readonly string[];\n\treadonly signals: readonly string[];\n")
out.WriteString("\treadonly applyToMetrics: readonly string[];\n")
out.WriteString("\treadonly valueMap: Readonly<Record<string, string>>;\n};\n\n")
out.WriteString("export const SEMCONV_FAMILIES: readonly SemconvFamily[] = [\n")
for _, family := range families {
out.WriteString("\t{\n")
fmt.Fprintf(&out, "\t\tcurrent: %s,\n", tsString(family.Current))
fmt.Fprintf(&out, "\t\told: %s,\n", tsStringSlice(family.Old))
fmt.Fprintf(&out, "\t\tkind: %s,\n", tsString(family.Kind))
fmt.Fprintf(&out, "\t\tcontexts: %s,\n", tsStringSlice(family.Contexts))
fmt.Fprintf(&out, "\t\tsignals: %s,\n", tsStringSlice(family.Signals))
fmt.Fprintf(&out, "\t\tapplyToMetrics: %s,\n", tsStringSlice(family.ApplyToMetrics))
out.WriteString("\t\tvalueMap: {")
keys := sortedMapKeys(family.ValueMap)
for i, key := range keys {
if i > 0 {
out.WriteString(", ")
}
fmt.Fprintf(&out, "%s: %s", tsString(key), tsString(family.ValueMap[key]))
}
out.WriteString("},\n\t},\n")
}
out.WriteString("] as const;\n")
return out.Bytes()
}
func tsString(value string) string {
quoted := strconv.Quote(value)
return "'" + strings.ReplaceAll(quoted[1:len(quoted)-1], "'", `\'`) + "'"
}
func tsStringSlice(values []string) string {
quoted := make([]string, len(values))
for i, value := range values {
quoted[i] = tsString(value)
}
return "[" + strings.Join(quoted, ", ") + "]"
}
func sortedMapKeys[T any](values map[string]T) []string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func checkFile(path string, expected []byte) error {
actual, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("generated file %s is missing: run go run ./scripts/semconv", path)
}
if !bytes.Equal(actual, expected) {
return fmt.Errorf("generated file %s is stale: run go run ./scripts/semconv", path)
}
return nil
}

View File

@@ -1,376 +0,0 @@
package main
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSchemaDecoderRejectsUnsupportedSection(t *testing.T) {
var schema schemaFile
err := decodeKnownFields([]byte(`
versions:
1.0.0:
span_events:
changes:
- rename_events:
event_map:
old: current
`), &schema)
assert.ErrorContains(t, err, "field span_events not found", "unsupported schema sections must fail generation")
}
func TestBuildFamiliesRejectsMalformedSchemaVersion(t *testing.T) {
var schema schemaFile
require.NoError(t, decodeKnownFields([]byte(`
versions:
latest:
spans:
changes: []
`), &schema), "test schema must decode")
_, err := buildFamilies([]schemaFile{schema}, overlayFile{})
assert.ErrorContains(t, err, `schema version "latest"`, "malformed versions must not be silently reordered")
}
func TestBuildFamiliesResolvesRenameChain(t *testing.T) {
var schema schemaFile
require.NoError(t, decodeKnownFields([]byte(`
versions:
4.0.0:
spans:
changes:
- rename_attributes:
attribute_map:
a: b
3.0.0:
spans:
changes:
- rename_attributes:
attribute_map:
b: c
x: c
2.0.0:
spans:
changes:
- rename_attributes:
attribute_map:
a: b
- rename_attributes:
attribute_map:
a: b
`), &schema), "test schema must decode")
enabled := true
families, err := buildFamilies([]schemaFile{schema}, overlayFile{Families: map[string]overlayFamily{
"c": {Enabled: &enabled},
}})
require.NoError(t, err)
assert.Equal(t, []generatedFamily{{
Current: "c",
Old: []string{"b", "x", "a"},
Kind: kindAttribute,
Contexts: []string{"attribute"},
Signals: []string{"traces"},
}}, families, "predecessors should be ordered by distance and then name")
}
func TestBuildFamiliesMapsSchemaSectionsToScopes(t *testing.T) {
var schema schemaFile
require.NoError(t, decodeKnownFields([]byte(`
versions:
1.0.0:
all:
changes:
- rename_attributes:
attribute_map:
all.old: all.current
resources:
changes:
- rename_attributes:
attribute_map:
resource.old: resource.current
logs:
changes:
- rename_attributes:
attribute_map:
log.old: log.current
metrics:
changes:
- rename_attributes:
attribute_map:
state: cpu.mode
apply_to_metrics: [system.cpu.time]
- rename_metrics:
old.metric: current.metric
`), &schema), "test schema must decode")
enabled := true
families, err := buildFamilies([]schemaFile{schema}, overlayFile{Families: map[string]overlayFamily{
"all.current": {Enabled: &enabled},
"resource.current": {Enabled: &enabled},
"log.current": {Enabled: &enabled},
"cpu.mode": {Enabled: &enabled},
"current.metric": {Enabled: &enabled, Kind: kindMetric},
}})
require.NoError(t, err)
assert.Equal(t, []generatedFamily{
{
Current: "all.current", Old: []string{"all.old"}, Kind: kindAttribute,
Contexts: nil, Signals: nil,
},
{
Current: "cpu.mode", Old: []string{"state"}, Kind: kindAttribute,
Contexts: []string{"attribute"}, Signals: []string{"metrics"},
ApplyToMetrics: []string{"system.cpu.time"},
},
{
Current: "current.metric", Old: []string{"old.metric"}, Kind: kindMetric,
Contexts: []string{"metric"}, Signals: []string{"metrics"},
},
{
Current: "log.current", Old: []string{"log.old"}, Kind: kindAttribute,
Contexts: []string{"attribute"}, Signals: []string{"logs"},
},
{
Current: "resource.current", Old: []string{"resource.old"}, Kind: kindAttribute,
Contexts: []string{"resource"},
},
}, families, "schema sections should produce their documented signal and context scopes")
}
func TestOverlayAddsFamilyWithoutSchemaHistory(t *testing.T) {
enabled := true
families, err := buildFamilies(nil, overlayFile{Families: map[string]overlayFamily{
"added.current": {
Enabled: &enabled,
Old: []string{"added.old"},
Contexts: []string{"resource"},
Signals: []string{"traces"},
},
}})
require.NoError(t, err)
assert.Equal(t, []generatedFamily{{
Current: "added.current",
Old: []string{"added.old"},
Kind: kindAttribute,
Contexts: []string{"resource"},
Signals: []string{"traces"},
}}, families, "an explicit overlay family should not require schema history")
}
func TestOverlayOverridesGeneratedFamily(t *testing.T) {
var schema schemaFile
require.NoError(t, decodeKnownFields([]byte(`
versions:
1.0.0:
spans:
changes:
- rename_attributes:
attribute_map:
old: current
`), &schema), "test schema must decode")
enabled := true
families, err := buildFamilies([]schemaFile{schema}, overlayFile{Families: map[string]overlayFamily{
"current": {
Enabled: &enabled,
AddOld: []string{"older"},
ExcludeOld: []string{"old"},
AddContexts: []string{"resource"},
AddSignals: []string{"logs"},
ValueMap: map[string]string{"legacy": "current"},
},
}})
require.NoError(t, err)
assert.Equal(t, []generatedFamily{{
Current: "current",
Old: []string{"older"},
Kind: kindAttribute,
Contexts: []string{"attribute", "resource"},
Signals: []string{"logs", "traces"},
ValueMap: map[string]string{"legacy": "current"},
}}, families, "overlay additions and exclusions should be applied to the generated family")
}
func TestOverlayDisablesFamilyWhenDefaultIsEnabled(t *testing.T) {
var schema schemaFile
require.NoError(t, decodeKnownFields([]byte(`
versions:
1.0.0:
spans:
changes:
- rename_attributes:
attribute_map:
old: current
`), &schema), "test schema must decode")
disabled := false
families, err := buildFamilies([]schemaFile{schema}, overlayFile{
DefaultEnabled: true,
Families: map[string]overlayFamily{
"current": {Enabled: &disabled},
},
})
require.NoError(t, err)
assert.Empty(t, families, "an explicitly disabled family must override default_enabled")
}
func TestRenderGoIsDeterministic(t *testing.T) {
families := []generatedFamily{{
Current: "current", Old: []string{"old"}, Kind: kindAttribute,
ValueMap: map[string]string{"b": "2", "a": "1"},
}}
first, err := renderGo(families)
require.NoError(t, err)
second, err := renderGo(families)
require.NoError(t, err)
assert.Equal(t, first, second, "Go generation must not depend on map iteration order")
}
func TestRenderGoUsesCanonicalTelemetryTypes(t *testing.T) {
families := []generatedFamily{{
Current: "current", Old: []string{"old"}, Kind: kindAttribute,
Contexts: []string{"resource"}, Signals: []string{"traces"},
}}
output, err := renderGo(families)
require.NoError(t, err)
assert.Contains(t, string(output), "telemetrytypes.FieldContextResource", "generated contexts should use telemetrytypes")
assert.Contains(t, string(output), "telemetrytypes.SignalTraces", "generated signals should use telemetrytypes")
}
func TestRenderTypeScriptIsDeterministic(t *testing.T) {
families := []generatedFamily{{
Current: "current", Old: []string{"old"}, Kind: kindAttribute,
ValueMap: map[string]string{"b": "2", "a": "1"},
}}
assert.Equal(t, renderTypeScript(families), renderTypeScript(families), "TypeScript generation must not depend on map iteration order")
}
func TestBuildFamiliesHandlesRenameRollback(t *testing.T) {
var schema schemaFile
require.NoError(t, decodeKnownFields([]byte(`
versions:
2.0.0:
metrics:
changes:
- rename_metrics:
temporary: original
1.0.0:
metrics:
changes:
- rename_metrics:
original: temporary
`), &schema), "test schema must decode")
enabled := true
families, err := buildFamilies([]schemaFile{schema}, overlayFile{Families: map[string]overlayFamily{
"original": {Enabled: &enabled, Kind: kindMetric},
}})
require.NoError(t, err)
assert.Equal(t, []generatedFamily{{
Current: "original",
Old: []string{"temporary"},
Kind: kindMetric,
Contexts: []string{"metric"},
Signals: []string{"metrics"},
}}, families, "the latest rollback destination should remain the family root")
}
func TestBuildFamiliesRejectsSameVersionRenameChain(t *testing.T) {
var schema schemaFile
require.NoError(t, decodeKnownFields([]byte(`
versions:
1.0.0:
spans:
changes:
- rename_attributes:
attribute_map:
x: y
y: z
`), &schema), "test schema must decode")
_, err := buildFamilies([]schemaFile{schema}, overlayFile{})
assert.ErrorContains(t, err, `same-version attribute rename chain through "y"`, "order-sensitive same-version chains must be rejected")
}
func TestBuildFamiliesRejectsOverlayFamilyWithoutHistory(t *testing.T) {
enabled := true
_, err := buildFamilies(nil, overlayFile{Families: map[string]overlayFamily{
"missing": {Enabled: &enabled},
}})
assert.ErrorContains(t, err, `overlay family "missing" with kind "attribute" is absent`, "an overlay cannot invent a family without old members")
}
func TestBuildFamiliesRejectsEnabledFamilyWithoutOldMembers(t *testing.T) {
var schema schemaFile
require.NoError(t, decodeKnownFields([]byte(`
versions:
1.0.0:
spans:
changes:
- rename_attributes:
attribute_map:
old: current
`), &schema), "test schema must decode")
enabled := true
_, err := buildFamilies([]schemaFile{schema}, overlayFile{Families: map[string]overlayFamily{
"current": {Enabled: &enabled, ExcludeOld: []string{"old"}},
}})
assert.ErrorContains(t, err, `enabled family "current" with kind "attribute" has no old members`, "exclude_old cannot empty an enabled family")
}
func TestOverlayKindDefaultsToAttribute(t *testing.T) {
var schema schemaFile
require.NoError(t, decodeKnownFields([]byte(`
versions:
1.0.0:
spans:
changes:
- rename_attributes:
attribute_map:
attribute.old: shared.current
metrics:
changes:
- rename_metrics:
metric.old: shared.current
`), &schema), "test schema must decode")
enabled := true
families, err := buildFamilies([]schemaFile{schema}, overlayFile{Families: map[string]overlayFamily{
"shared.current": {Enabled: &enabled},
}})
require.NoError(t, err)
assert.Equal(t, []generatedFamily{{
Current: "shared.current",
Old: []string{"attribute.old"},
Kind: kindAttribute,
Contexts: []string{"attribute"},
Signals: []string{"traces"},
}}, families, "a kind-less overlay policy should affect only the attribute family")
}
func TestCheckFileReportsStaleOutput(t *testing.T) {
path := filepath.Join(t.TempDir(), "generated.go")
require.NoError(t, os.WriteFile(path, []byte("old"), 0o600), "test output must be writable")
assert.ErrorContains(t, checkFile(path, []byte("new")), "is stale", "check mode must reject stale generated output")
}
func TestTypeScriptStringEscapesControlCharacters(t *testing.T) {
assert.Equal(t, `'line\n\t\x01\'\\end'`, tsString("line\n\t\x01'\\end"), "generated TypeScript strings must remain valid literals")
}

View File

@@ -1,11 +0,0 @@
# SigNoz semantic-convention rollout policy.
#
# Families are keyed by their current OpenTelemetry name. Schema-derived
# families are disabled by default so rollout remains explicit and reversible.
default_enabled: false
families:
deployment.environment.name:
enabled: true
db.system.name:
enabled: true

View File

@@ -1,760 +0,0 @@
file_format: 1.1.0
schema_url: https://opentelemetry.io/schemas/1.42.0
versions:
1.42.0:
metrics:
changes:
- rename_metrics:
v8js.memory.heap.limit: v8js.memory.heap.space.size
1.41.1:
1.41.0:
metrics:
changes:
- rename_metrics:
k8s.container.cpu.limit: k8s.container.cpu.limit.desired
k8s.container.cpu.limit_utilization: k8s.container.cpu.limit.utilization
k8s.container.cpu.request: k8s.container.cpu.request.desired
k8s.container.cpu.request_utilization: k8s.container.cpu.request.utilization
k8s.container.memory.limit: k8s.container.memory.limit.desired
k8s.container.memory.request: k8s.container.memory.request.desired
1.40.0:
all:
changes:
- rename_attributes:
attribute_map:
feature_flag.evaluation.error.message: feature_flag.error.message
metrics:
changes:
- rename_metrics:
system.memory.shared: system.memory.linux.shared
1.39.0:
all:
changes:
- rename_attributes:
attribute_map:
linux.memory.slab.state: system.memory.linux.slab.state
peer.service: service.peer.name
rpc.connect_rpc.error_code: rpc.response.status_code
rpc.connect_rpc.request.metadata: rpc.request.metadata
rpc.connect_rpc.response.metadata: rpc.response.metadata
rpc.grpc.request.metadata: rpc.request.metadata
rpc.grpc.response.metadata: rpc.response.metadata
rpc.jsonrpc.request_id: jsonrpc.request.id
rpc.jsonrpc.version: jsonrpc.protocol.version
rpc.system: rpc.system.name
metrics:
changes:
- rename_metrics:
process.open_file_descriptor.count: process.unix.file_descriptor.count
system.linux.memory.available: system.memory.linux.available
system.linux.memory.slab.usage: system.memory.linux.slab.usage
1.38.0:
all:
changes:
- rename_attributes:
attribute_map:
process.context_switch_type: process.context_switch.type
process.paging.fault_type: system.paging.fault.type
system.cpu.logical_number: cpu.logical_number
system.paging.type: system.paging.fault.type
system.process.status: process.state
system.processes.status: process.state
metrics:
changes:
- rename_metrics:
k8s.cronjob.active_jobs: k8s.cronjob.job.active
k8s.daemonset.current_scheduled_nodes: k8s.daemonset.node.current_scheduled
k8s.daemonset.desired_scheduled_nodes: k8s.daemonset.node.desired_scheduled
k8s.daemonset.misscheduled_nodes: k8s.daemonset.node.misscheduled
k8s.daemonset.ready_nodes: k8s.daemonset.node.ready
k8s.deployment.available_pods: k8s.deployment.pod.available
k8s.deployment.desired_pods: k8s.deployment.pod.desired
k8s.hpa.current_pods: k8s.hpa.pod.current
k8s.hpa.desired_pods: k8s.hpa.pod.desired
k8s.hpa.max_pods: k8s.hpa.pod.max
k8s.hpa.min_pods: k8s.hpa.pod.min
k8s.job.active_pods: k8s.job.pod.active
k8s.job.desired_successful_pods: k8s.job.pod.desired_successful
k8s.job.failed_pods: k8s.job.pod.failed
k8s.job.max_parallel_pods: k8s.job.pod.max_parallel
k8s.job.successful_pods: k8s.job.pod.successful
k8s.node.allocatable.cpu: k8s.node.cpu.allocatable
k8s.node.allocatable.ephemeral_storage: k8s.node.ephemeral_storage.allocatable
k8s.node.allocatable.memory: k8s.node.memory.allocatable
k8s.node.allocatable.pods: k8s.node.pod.allocatable
k8s.replicaset.available_pods: k8s.replicaset.pod.available
k8s.replicaset.desired_pods: k8s.replicaset.pod.desired
k8s.replication_controller.available_pods: k8s.replicationcontroller.pod.available
k8s.replication_controller.desired_pods: k8s.replicationcontroller.pod.desired
k8s.replicationcontroller.available_pods: k8s.replicationcontroller.pod.available
k8s.replicationcontroller.desired_pods: k8s.replicationcontroller.pod.desired
k8s.statefulset.current_pods: k8s.statefulset.pod.current
k8s.statefulset.desired_pods: k8s.statefulset.pod.desired
k8s.statefulset.ready_pods: k8s.statefulset.pod.ready
k8s.statefulset.updated_pods: k8s.statefulset.pod.updated
v8js.heap.space.available_size: v8js.memory.heap.space.available_size
v8js.heap.space.physical_size: v8js.memory.heap.space.physical_size
1.37.0:
all:
changes:
- rename_attributes:
attribute_map:
android.state: android.app.state
container.runtime: container.runtime.name
enduser.role: user.roles
gen_ai.openai.request.service_tier: openai.request.service_tier
gen_ai.openai.response.service_tier: openai.response.service_tier
gen_ai.openai.response.system_fingerprint: openai.response.system_fingerprint
gen_ai.system: gen_ai.provider.name
ios.state: ios.app.state
1.36.0:
1.35.0:
all:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/1698
- rename_attributes:
attribute_map:
az.namespace: azure.resource_provider.namespace
az.service_request_id: azure.service.request.id
metrics:
changes:
# https://github.com/open-telemetry/semantic-conventions/issues/1800
- rename_metrics:
system.network.connections: system.network.connection.count
1.34.0:
metrics:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/2295
- rename_metrics:
cpu.time: system.cpu.time
cpu.utilization: system.cpu.utilization
cpu.frequency: system.cpu.frequency
1.33.0:
all:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/1982
- rename_attributes:
attribute_map:
feature_flag.provider_name: feature_flag.provider.name
# https://github.com/open-telemetry/semantic-conventions/pull/1994
- rename_attributes:
attribute_map:
feature_flag.evaluation.error.message: error.message
1.32.0:
all:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/1989
- rename_attributes:
attribute_map:
feature_flag.evaluation.reason: feature_flag.result.reason
feature_flag.variant: feature_flag.result.variant
metrics:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/2042
- rename_metrics:
otel.sdk.span.live.count: otel.sdk.span.live
otel.sdk.span.ended.count: otel.sdk.span.ended
otel.sdk.processor.span.processed.count: otel.sdk.processor.span.processed
otel.sdk.exporter.span.inflight.count: otel.sdk.exporter.span.inflight
otel.sdk.exporter.span.exported.count: otel.sdk.exporter.span.exported
1.31.0:
all:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/1880
- rename_attributes:
attribute_map:
android.state: android.app.state
io.state: ios.app.state
metrics:
changes:
- rename_metrics:
k8s.replication_controller.desired_pods: k8s.replicationcontroller.desired_pods
k8s.replication_controller.available_pods: k8s.replicationcontroller.available_pods
# https://github.com/open-telemetry/semantic-conventions/pull/1896
- rename_metrics:
system.cpu.time: cpu.time
system.cpu.utilization: cpu.utilization
system.cpu.frequency: cpu.frequency
# https://github.com/open-telemetry/semantic-conventions/pull/1896
- rename_attributes:
attribute_map:
system.cpu.logical_number: cpu.logical_number
1.30.0:
all:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/1632
- rename_attributes:
attribute_map:
gen_ai.openai.request.seed: gen_ai.request.seed
system.network.state: network.connection.state
# https://github.com/open-telemetry/semantic-conventions/pull/1624
- rename_attributes:
attribute_map:
code.function: code.function.name
code.filepath: code.file.path
code.lineno: code.line.number
code.column: code.column.number
# https://github.com/open-telemetry/semantic-conventions/pull/1734
- rename_attributes:
attribute_map:
db.system: db.system.name
db.cassandra.coordinator.dc: cassandra.coordinator.dc
db.cassandra.coordinator.id: cassandra.coordinator.id
db.cassandra.consistency_level: cassandra.consistency.level
db.cassandra.idempotence: cassandra.query.idempotent
db.cassandra.page_size: cassandra.page.size
db.cassandra.speculative_execution_count: cassandra.speculative_execution.count
db.cosmosdb.client_id: azure.client.id
db.cosmosdb.connection_mode: azure.cosmosdb.connection.mode
db.cosmosdb.consistency_level: azure.cosmosdb.consistency.level
db.cosmosdb.request_charge: azure.cosmosdb.operation.request_charge
db.cosmosdb.request_content_length: azure.cosmosdb.request.body.size
db.cosmosdb.regions_contacted: azure.cosmosdb.operation.contacted_regions
db.cosmosdb.sub_status_code: azure.cosmosdb.response.sub_status_code
db.elasticsearch.node.name: elasticsearch.node.name
# db.elasticsearch.path_parts is a template attribute, schema transformation
# does not support it, adding as a comment for consistency
# db.elasticsearch.path_parts.<key> -> db.operation.parameter.<key>
metrics:
changes:
- rename_metrics:
db.client.cosmosdb.operation.request_charge: azure.cosmosdb.client.operation.request_charge
db.client.cosmosdb.active_instance.count: azure.cosmosdb.client.active_instance.count
1.29.0:
all:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/1520
- rename_attributes:
attribute_map:
process.executable.build_id.profiling: process.executable.build_id.htlhash
# https://github.com/open-telemetry/semantic-conventions/pull/1383
- rename_attributes:
attribute_map:
vcs.repository.change.id: vcs.change.id
vcs.repository.change.title: vcs.change.title
vcs.repository.ref.name: vcs.ref.head.name
vcs.repository.ref.revision: vcs.ref.head.revision
vcs.repository.ref.type: vcs.ref.head.type
metrics:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/1492
- rename_attributes:
attribute_map:
system.device: network.interface.name
apply_to_metrics:
- container.network.io
- system.network.dropped
- system.network.errors
- system.network.io
- system.network.connections
1.28.0:
metrics:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/1422
- rename_metrics:
messaging.client.published.messages: messaging.client.sent.messages
1.27.0:
all:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/1216
- rename_attributes:
attribute_map:
tls.client.server_name: server.address
# https://github.com/open-telemetry/semantic-conventions/pull/1075
- rename_attributes:
attribute_map:
deployment.environment: deployment.environment.name
# https://github.com/open-telemetry/semantic-conventions/pull/1245
- rename_attributes:
attribute_map:
messaging.kafka.message.offset: messaging.kafka.offset
# https://github.com/open-telemetry/semantic-conventions/pull/815
- rename_attributes:
attribute_map:
messaging.kafka.consumer.group: messaging.consumer.group.name
messaging.rocketmq.client_group: messaging.consumer.group.name
messaging.eventhubs.consumer.group: messaging.consumer.group.name
messaging.servicebus.destination.subscription_name: messaging.destination.subscription.name
# https://github.com/open-telemetry/semantic-conventions/pull/1200
- rename_attributes:
attribute_map:
gen_ai.usage.completion_tokens: gen_ai.usage.output_tokens
gen_ai.usage.prompt_tokens: gen_ai.usage.input_tokens
spans:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/1002
- rename_attributes:
attribute_map:
db.elasticsearch.cluster.name: db.namespace
metrics:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/1125
- rename_attributes:
attribute_map:
db.client.connections.state: db.client.connection.state
apply_to_metrics:
- db.client.connection.count
- rename_attributes:
attribute_map:
db.client.connections.pool.name: db.client.connection.pool.name
apply_to_metrics:
- db.client.connection.count
- db.client.connection.idle.max
- db.client.connection.idle.min
- db.client.connection.max
- db.client.connection.pending_requests
- db.client.connection.timeouts
- db.client.connection.create_time
- db.client.connection.wait_time
- db.client.connection.use_time
# https://github.com/open-telemetry/semantic-conventions/pull/1006
- rename_metrics:
messaging.publish.messages: messaging.client.published.messages
# https://github.com/open-telemetry/semantic-conventions/pull/1026
- rename_attributes:
attribute_map:
system.cpu.state: cpu.mode
process.cpu.state: cpu.mode
container.cpu.state: cpu.mode
apply_to_metrics:
- system.cpu.time
- system.cpu.utilization
- process.cpu.time
- process.cpu.utilization
- container.cpu.time
# https://github.com/open-telemetry/semantic-conventions/pull/1265
- rename_metrics:
jvm.buffer.memory.usage: jvm.buffer.memory.used
1.26.0:
metrics:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/966
- rename_metrics:
db.client.connections.usage: db.client.connection.count
db.client.connections.idle.max: db.client.connection.idle.max
db.client.connections.idle.min: db.client.connection.idle.min
db.client.connections.max: db.client.connection.max
db.client.connections.pending_requests: db.client.connection.pending_requests
db.client.connections.timeouts: db.client.connection.timeouts
# https://github.com/open-telemetry/semantic-conventions/pull/948
- rename_attributes:
attribute_map:
messaging.client_id: messaging.client.id
# https://github.com/open-telemetry/semantic-conventions/pull/909
- rename_attributes:
attribute_map:
state: db.client.connections.state
apply_to_metrics:
- db.client.connections.usage
- rename_attributes:
attribute_map:
pool.name: db.client.connections.pool.name
apply_to_metrics:
- db.client.connections.usage
- db.client.connections.idle.max
- db.client.connections.idle.min
- db.client.connections.max
- db.client.connections.pending_requests
- db.client.connections.timeouts
- db.client.connections.create_time
- db.client.connections.wait_time
- db.client.connections.use_time
all:
changes:
# https://github:com/open-telemetry/semantic-conventions/pull/731/
- rename_attributes:
attribute_map:
enduser.id: user.id
1.25.0:
spans:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/911
- rename_attributes:
attribute_map:
db.name: db.namespace
# https://github.com/open-telemetry/semantic-conventions/pull/870
- rename_attributes:
attribute_map:
db.sql.table: db.collection.name
db.mongodb.collection: db.collection.name
db.cosmosdb.container: db.collection.name
db.cassandra.table: db.collection.name
# https://github.com/open-telemetry/semantic-conventions/pull/798
- rename_attributes:
attribute_map:
messaging.kafka.destination.partition: messaging.destination.partition.id
# https://github.com/open-telemetry/semantic-conventions/pull/875
- rename_attributes:
attribute_map:
db.operation: db.operation.name
# https://github.com/open-telemetry/semantic-conventions/pull/913
- rename_attributes:
attribute_map:
messaging.operation: messaging.operation.type
# https://github.com/open-telemetry/semantic-conventions/pull/866
- rename_attributes:
attribute_map:
db.statement: db.query.text
metrics:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/484
- rename_attributes:
attribute_map:
system.processes.status: system.process.status
apply_to_metrics:
- system.processes.count
- rename_metrics:
system.processes.count: system.process.count
system.processes.created: system.process.created
# https://github.com/open-telemetry/semantic-conventions/pull/625
- rename_attributes:
attribute_map:
container.labels: container.label
k8s.pod.labels: k8s.pod.label
# https://github.com/open-telemetry/semantic-conventions/pull/330
- rename_metrics:
process.threads: process.thread.count
process.open_file_descriptors: process.open_file_descriptor.count
- rename_attributes:
attribute_map:
state: process.cpu.state
apply_to_metrics:
- process.cpu.time
- process.cpu.utilization
- rename_attributes:
attribute_map:
direction: disk.io.direction
apply_to_metrics:
- process.disk.io
- rename_attributes:
attribute_map:
type: process.context_switch_type
apply_to_metrics:
- process.context_switches
- rename_attributes:
attribute_map:
direction: network.io.direction
apply_to_metrics:
- process.network.io
- rename_attributes:
attribute_map:
type: process.paging.fault_type
apply_to_metrics:
- process.paging.faults
all:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/854
- rename_attributes:
attribute_map:
message.type: rpc.message.type
message.id: rpc.message.id
message.compressed_size: rpc.message.compressed_size
message.uncompressed_size: rpc.message.uncompressed_size
1.24.0:
metrics:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/536
- rename_metrics:
jvm.memory.usage: jvm.memory.used
jvm.memory.usage_after_last_gc: jvm.memory.used_after_last_gc
# https://github.com/open-telemetry/semantic-conventions/pull/530
- rename_attributes:
attribute_map:
system.network.io.direction: network.io.direction
system.disk.io.direction: disk.io.direction
1.23.1:
1.23.0:
metrics:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/20
- rename_attributes:
attribute_map:
thread.daemon: jvm.thread.daemon
apply_to_metrics:
- jvm.thread.count
1.22.0:
spans:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/229
- rename_attributes:
attribute_map:
messaging.message.payload_size_bytes: messaging.message.body.size
# https://github.com/open-telemetry/opentelemetry-specification/pull/374
- rename_attributes:
attribute_map:
http.resend_count: http.request.resend_count
metrics:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/224
- rename_metrics:
http.client.duration: http.client.request.duration
http.server.duration: http.server.request.duration
# https://github.com/open-telemetry/semantic-conventions/pull/241
- rename_metrics:
process.runtime.jvm.memory.usage: jvm.memory.usage
process.runtime.jvm.memory.committed: jvm.memory.committed
process.runtime.jvm.memory.limit: jvm.memory.limit
process.runtime.jvm.memory.usage_after_last_gc: jvm.memory.usage_after_last_gc
process.runtime.jvm.gc.duration: jvm.gc.duration
# also https://github.com/open-telemetry/semantic-conventions/pull/252
process.runtime.jvm.threads.count: jvm.thread.count
# also https://github.com/open-telemetry/semantic-conventions/pull/252
process.runtime.jvm.classes.loaded: jvm.class.loaded
# also https://github.com/open-telemetry/semantic-conventions/pull/252
process.runtime.jvm.classes.unloaded: jvm.class.unloaded
# also https://github.com/open-telemetry/semantic-conventions/pull/252
# and https://github.com/open-telemetry/semantic-conventions/pull/60
process.runtime.jvm.classes.current_loaded: jvm.class.count
process.runtime.jvm.cpu.time: jvm.cpu.time
process.runtime.jvm.cpu.recent_utilization: jvm.cpu.recent_utilization
process.runtime.jvm.memory.init: jvm.memory.init
process.runtime.jvm.system.cpu.utilization: jvm.system.cpu.utilization
process.runtime.jvm.system.cpu.load_1m: jvm.system.cpu.load_1m
# https://github.com/open-telemetry/semantic-conventions/pull/253
process.runtime.jvm.buffer.usage: jvm.buffer.memory.usage
# https://github.com/open-telemetry/semantic-conventions/pull/253
process.runtime.jvm.buffer.limit: jvm.buffer.memory.limit
process.runtime.jvm.buffer.count: jvm.buffer.count
# https://github.com/open-telemetry/semantic-conventions/pull/20
- rename_attributes:
attribute_map:
type: jvm.memory.type
pool: jvm.memory.pool.name
apply_to_metrics:
- jvm.memory.usage
- jvm.memory.committed
- jvm.memory.limit
- jvm.memory.usage_after_last_gc
- jvm.memory.init
- rename_attributes:
attribute_map:
name: jvm.gc.name
action: jvm.gc.action
apply_to_metrics:
- jvm.gc.duration
- rename_attributes:
attribute_map:
daemon: thread.daemon
apply_to_metrics:
- jvm.threads.count
- rename_attributes:
attribute_map:
pool: jvm.buffer.pool.name
apply_to_metrics:
- jvm.buffer.memory.usage
- jvm.buffer.memory.limit
- jvm.buffer.count
# https://github.com/open-telemetry/semantic-conventions/pull/89
- rename_attributes:
attribute_map:
state: system.cpu.state
cpu: system.cpu.logical_number
apply_to_metrics:
- system.cpu.time
- system.cpu.utilization
- rename_attributes:
attribute_map:
state: system.memory.state
apply_to_metrics:
- system.memory.usage
- system.memory.utilization
- rename_attributes:
attribute_map:
state: system.paging.state
apply_to_metrics:
- system.paging.usage
- system.paging.utilization
- rename_attributes:
attribute_map:
type: system.paging.type
direction: system.paging.direction
apply_to_metrics:
- system.paging.faults
- system.paging.operations
- rename_attributes:
attribute_map:
device: system.device
direction: system.disk.direction
apply_to_metrics:
- system.disk.io
- system.disk.operations
- system.disk.io_time
- system.disk.operation_time
- system.disk.merged
- rename_attributes:
attribute_map:
device: system.device
state: system.filesystem.state
type: system.filesystem.type
mode: system.filesystem.mode
mountpoint: system.filesystem.mountpoint
apply_to_metrics:
- system.filesystem.usage
- system.filesystem.utilization
- rename_attributes:
attribute_map:
device: system.device
direction: system.network.direction
protocol: network.protocol
state: system.network.state
apply_to_metrics:
- system.network.dropped
- system.network.packets
- system.network.errors
- system.network.io
- system.network.connections
- rename_attributes:
attribute_map:
status: system.processes.status
apply_to_metrics:
- system.processes.count
# https://github.com/open-telemetry/semantic-conventions/pull/247
- rename_metrics:
http.server.request.size: http.server.request.body.size
http.server.response.size: http.server.response.body.size
resources:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/178
- rename_attributes:
attribute_map:
telemetry.auto.version: telemetry.distro.version
1.21.0:
spans:
changes:
# https://github.com/open-telemetry/opentelemetry-specification/pull/3336
- rename_attributes:
attribute_map:
messaging.kafka.client_id: messaging.client_id
messaging.rocketmq.client_id: messaging.client_id
# https://github.com/open-telemetry/opentelemetry-specification/pull/3402
- rename_attributes:
attribute_map:
# net.peer.(name|port) attributes were usually populated on client side
# so they should be usually translated to server.(address|port)
# net.host.* attributes were only populated on server side
net.host.name: server.address
net.host.port: server.port
# was only populated on client side
net.sock.peer.name: server.socket.domain
# net.sock.peer.(addr|port) mapping is not possible
# since they applied to both client and server side
# were only populated on server side
net.sock.host.addr: server.socket.address
net.sock.host.port: server.socket.port
http.client_ip: client.address
# https://github.com/open-telemetry/opentelemetry-specification/pull/3426
- rename_attributes:
attribute_map:
net.protocol.name: network.protocol.name
net.protocol.version: network.protocol.version
net.host.connection.type: network.connection.type
net.host.connection.subtype: network.connection.subtype
net.host.carrier.name: network.carrier.name
net.host.carrier.mcc: network.carrier.mcc
net.host.carrier.mnc: network.carrier.mnc
net.host.carrier.icc: network.carrier.icc
# https://github.com/open-telemetry/opentelemetry-specification/pull/3355
- rename_attributes:
attribute_map:
http.method: http.request.method
http.status_code: http.response.status_code
http.scheme: url.scheme
http.url: url.full
http.request_content_length: http.request.body.size
http.response_content_length: http.response.body.size
metrics:
changes:
# https://github.com/open-telemetry/semantic-conventions/pull/53
- rename_metrics:
process.runtime.jvm.cpu.utilization: process.runtime.jvm.cpu.recent_utilization
1.20.0:
spans:
changes:
# https://github.com/open-telemetry/opentelemetry-specification/pull/3272
- rename_attributes:
attribute_map:
net.app.protocol.name: net.protocol.name
net.app.protocol.version: net.protocol.version
1.19.0:
spans:
changes:
# https://github.com/open-telemetry/opentelemetry-specification/pull/3209
- rename_attributes:
attribute_map:
faas.execution: faas.invocation_id
# https://github.com/open-telemetry/opentelemetry-specification/pull/3188
- rename_attributes:
attribute_map:
faas.id: cloud.resource_id
# https://github.com/open-telemetry/opentelemetry-specification/pull/3190
- rename_attributes:
attribute_map:
http.user_agent: user_agent.original
resources:
changes:
# https://github.com/open-telemetry/opentelemetry-specification/pull/3190
- rename_attributes:
attribute_map:
browser.user_agent: user_agent.original
1.18.0:
1.17.0:
spans:
changes:
# https://github.com/open-telemetry/opentelemetry-specification/pull/2957
- rename_attributes:
attribute_map:
messaging.consumer_id: messaging.consumer.id
messaging.protocol: net.app.protocol.name
messaging.protocol_version: net.app.protocol.version
messaging.destination: messaging.destination.name
messaging.temp_destination: messaging.destination.temporary
messaging.destination_kind: messaging.destination.kind
messaging.message_id: messaging.message.id
messaging.conversation_id: messaging.message.conversation_id
messaging.message_payload_size_bytes: messaging.message.payload_size_bytes
messaging.message_payload_compressed_size_bytes: messaging.message.payload_compressed_size_bytes
messaging.rabbitmq.routing_key: messaging.rabbitmq.destination.routing_key
messaging.kafka.message_key: messaging.kafka.message.key
messaging.kafka.partition: messaging.kafka.destination.partition
messaging.kafka.tombstone: messaging.kafka.message.tombstone
messaging.rocketmq.message_type: messaging.rocketmq.message.type
messaging.rocketmq.message_tag: messaging.rocketmq.message.tag
messaging.rocketmq.message_keys: messaging.rocketmq.message.keys
messaging.kafka.consumer_group: messaging.kafka.consumer.group
1.16.0:
1.15.0:
spans:
changes:
# https://github.com/open-telemetry/opentelemetry-specification/pull/2743
- rename_attributes:
attribute_map:
http.retry_count: http.resend_count
1.14.0:
1.13.0:
spans:
changes:
# https://github.com/open-telemetry/opentelemetry-specification/pull/2614
- rename_attributes:
attribute_map:
net.peer.ip: net.sock.peer.addr
net.host.ip: net.sock.host.addr
1.12.0:
1.11.0:
1.10.0:
1.9.0:
1.8.0:
spans:
changes:
- rename_attributes:
attribute_map:
db.cassandra.keyspace: db.name
db.hbase.namespace: db.name
1.7.0:
1.6.1:
1.5.0:
1.4.0:

View File

@@ -18,7 +18,6 @@ pytest_plugins = [
"fixtures.logs",
"fixtures.traces",
"fixtures.metrics",
"fixtures.queriercommon",
"fixtures.metadata",
"fixtures.meter",
"fixtures.browser",

View File

@@ -329,6 +329,9 @@ def clickhouse(
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.TestContainerClickhouse:
"""
Package-scoped fixture for Clickhouse TestContainer.
"""
return create_clickhouse(
tmpfs=tmpfs,
network=network,

View File

@@ -1,3 +1,5 @@
"""Fixtures for cloud integration tests."""
from collections.abc import Callable
from dataclasses import dataclass, field
from http import HTTPStatus

View File

@@ -1,30 +0,0 @@
from http import HTTPStatus
import requests
from fixtures import types
DASHBOARDS_BASE_URL = "/api/v2/dashboards"
# MaxListLimit caps a single list page, so wiping a shared DB has to drain pages
# until the list comes back empty.
MAX_LIST_LIMIT = 200
def delete_all_dashboards(signoz: types.SigNoz, token: str) -> None:
while True:
response = requests.get(
signoz.self.host_configs["8080"].get(f"{DASHBOARDS_BASE_URL}?limit={MAX_LIST_LIMIT}"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
dashboards = response.json()["data"]["dashboards"]
if not dashboards:
return
for dashboard in dashboards:
del_res = requests.delete(
signoz.self.host_configs["8080"].get(f"{DASHBOARDS_BASE_URL}/{dashboard['id']}"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert del_res.status_code == HTTPStatus.NO_CONTENT, del_res.text

View File

@@ -24,6 +24,9 @@ def zeus(
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.TestContainerDocker:
"""
Package-scoped fixture for running zeus
"""
def create() -> types.TestContainerDocker:
container = WireMockContainer(image="wiremock/wiremock:2.35.1-1", secure=False)
@@ -73,6 +76,9 @@ def gateway(
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.TestContainerDocker:
"""
Package-scoped fixture for running gateway
"""
def create() -> types.TestContainerDocker:
container = WireMockContainer(image="wiremock/wiremock:2.35.1-1", secure=False)

23
tests/fixtures/idp.py vendored
View File

@@ -7,7 +7,6 @@ import pytest
import requests
from keycloak import KeycloakAdmin
from selenium import webdriver
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
@@ -371,26 +370,18 @@ def idp_login(driver: webdriver.Chrome) -> Callable[[str, str], None]:
password_field.send_keys(password)
# Click the login button
idp_host = urlparse(driver.current_url).netloc
login_button = wait.until(EC.element_to_be_clickable((By.ID, "kc-login")))
login_button.click()
# Wait till the browser has left the idp host — not just the login page: keycloak's SAML flow inserts an
# auto-submitting interstitial on the idp whose POST is what creates the user in signoz. The button is
# re-queried per poll; a mid-navigation WebDriverException (detached node) just retries the poll.
def _left_idp(drv: webdriver.Chrome) -> bool:
try:
return urlparse(drv.current_url).netloc != idp_host and not drv.find_elements(By.ID, "kc-login")
except WebDriverException:
return False
wait.until(_left_idp)
# Wait till kc-login element has vanished from the page, which means that a redirection is taking place.
wait.until(EC.invisibility_of_element((By.ID, "kc-login")))
return _idp_login
@pytest.fixture(name="create_group_idp", scope="function")
def create_group_idp(idp: types.TestContainerIDP) -> Callable[[str], str]:
"""Creates a group in Keycloak IDP."""
client = KeycloakAdmin(
server_url=idp.container.host_configs["6060"].base(),
username=IDP_ROOT_USERNAME,
@@ -419,6 +410,7 @@ def create_user_idp_with_groups(
idp: types.TestContainerIDP,
create_group_idp: Callable[[str], str], # pylint: disable=redefined-outer-name
) -> Callable[[str, str, bool, list[str]], None]:
"""Creates a user in Keycloak IDP with specified groups."""
client = KeycloakAdmin(
server_url=idp.container.host_configs["6060"].base(),
username=IDP_ROOT_USERNAME,
@@ -466,6 +458,7 @@ def add_user_to_group(
idp: types.TestContainerIDP,
create_group_idp: Callable[[str], str], # pylint: disable=redefined-outer-name
) -> Callable[[str, str], None]:
"""Adds an existing user to a group."""
client = KeycloakAdmin(
server_url=idp.container.host_configs["6060"].base(),
username=IDP_ROOT_USERNAME,
@@ -486,6 +479,7 @@ def create_user_idp_with_role(
idp: types.TestContainerIDP,
create_group_idp: Callable[[str], str], # pylint: disable=redefined-outer-name
) -> Callable[[str, str, bool, str, list[str]], None]:
"""Creates a user in Keycloak IDP with a custom role attribute and optional groups."""
client = KeycloakAdmin(
server_url=idp.container.host_configs["6060"].base(),
username=IDP_ROOT_USERNAME,
@@ -533,6 +527,7 @@ def create_user_idp_with_role(
@pytest.fixture(name="setup_user_profile", scope="package")
def setup_user_profile(idp: types.TestContainerIDP) -> Callable[[], None]:
"""Setup Keycloak User Profile with signoz_role attribute."""
def _setup_user_profile() -> None:
client = KeycloakAdmin(
@@ -573,6 +568,7 @@ def setup_user_profile(idp: types.TestContainerIDP) -> Callable[[], None]:
def _ensure_groups_client_scope(client: KeycloakAdmin) -> None:
"""Create 'groups' client scope if it doesn't exist."""
# Check if groups scope exists
scopes = client.get_client_scopes()
groups_scope_exists = any(s.get("name") == "groups" for s in scopes)
@@ -623,6 +619,7 @@ def _ensure_groups_client_scope(client: KeycloakAdmin) -> None:
def get_oidc_domain(signoz: types.SigNoz, admin_token: str) -> dict:
"""Helper to get the OIDC domain."""
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/domains"),
headers={"Authorization": f"Bearer {admin_token}"},
@@ -635,6 +632,7 @@ def get_oidc_domain(signoz: types.SigNoz, admin_token: str) -> dict:
def get_user_by_email(signoz: types.SigNoz, admin_token: str, email: str) -> dict:
"""Helper to get a user by email."""
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/user"),
timeout=2,
@@ -655,6 +653,7 @@ def perform_oidc_login(
email: str,
password: str,
) -> None:
"""Helper to perform OIDC login flow."""
session_context = get_session_context(email)
url = session_context["orgs"][0]["authNSupport"]["callback"][0]["url"]
parsed_url = urlparse(url)

View File

@@ -1,3 +1,5 @@
"""Shared constants/helpers for v2 infra-monitoring pod-status tests."""
# All 18 PodCountsByStatus buckets (camelCase, matches inframonitoringtypes.PodCountsByStatus / the API response).
STATUS_BUCKETS = (
"pending",
@@ -48,22 +50,3 @@ def expected_status_counts(**nonzero: int) -> dict:
counts = {bucket: 0 for bucket in STATUS_BUCKETS}
counts.update(nonzero)
return counts
# All buckets of the clusters-API per-group resource counts (camelCase, matches
# inframonitoringtypes ClusterRecord.Counts / the API response).
RESOURCE_COUNT_BUCKETS = (
"nodes",
"namespaces",
"deployments",
"daemonSets",
"jobs",
"statefulSets",
)
def expected_resource_counts(**nonzero: int) -> dict:
"""Full resource-counts dict with the given buckets set, rest 0."""
counts = {bucket: 0 for bucket in RESOURCE_COUNT_BUCKETS}
counts.update(nonzero)
return counts

View File

@@ -1,3 +1,9 @@
"""
Simpler version of metadataexporter for exporting jsontypes for test fixtures.
This exports JSON type metadata to the path_types table by parsing JSON bodies
and extracting all paths with their types, similar to how the real metadataexporter works.
"""
import datetime
import json
from abc import ABC
@@ -15,6 +21,8 @@ from fixtures import types
class JSONPathType(ABC):
"""Represents a JSON path with its type information"""
field_name: str
field_data_type: str
last_seen: np.uint64
@@ -36,6 +44,7 @@ class JSONPathType(ABC):
self.last_seen = np.uint64(int(last_seen.timestamp() * 1e9))
def np_arr(self) -> np.array:
"""Return path type data as numpy array for database insertion"""
return np.array([self.signal, self.field_context, self.field_name, self.field_data_type, self.last_seen])
@@ -136,7 +145,7 @@ def _python_type_to_clickhouse_type(value: Any) -> str:
elif isinstance(value, dict):
return "json"
else:
return "string"
return "string" # Default fallback
def _extract_json_paths(
@@ -145,7 +154,19 @@ def _extract_json_paths(
path_types: dict[str, set[str]] | None = None,
level: int = 0,
) -> dict[str, set[str]]:
"""Matches metadataexporter's analyzePValue logic."""
"""
Recursively extract all paths and their types from a JSON object.
Matches metadataexporter's analyzePValue logic.
Args:
obj: The JSON object to traverse
current_path: Current path being built (e.g., "user.name")
path_types: Dictionary mapping paths to sets of types found
level: Current nesting level (for depth limiting)
Returns:
Dictionary mapping paths to sets of type strings
"""
if path_types is None:
path_types = {}
@@ -158,14 +179,17 @@ def _extract_json_paths(
# Matches Go walkMap which recurses without calling ta.record on the map node.
for key, value in obj.items():
# Build the path for this key
if current_path:
new_path = f"{current_path}.{key}"
else:
new_path = key
# Recurse into the value
_extract_json_paths(value, new_path, path_types, level + 1)
elif isinstance(obj, list):
# Skip empty arrays
if len(obj) == 0:
return path_types
@@ -222,6 +246,17 @@ def _parse_json_bodies_and_extract_paths(
json_bodies: list[str],
timestamp: datetime.datetime | None = None,
) -> list[JSONPathType]:
"""
Parse JSON bodies and extract all paths with their types.
This mimics the behavior of metadataexporter.
Args:
json_bodies: List of JSON body strings to parse
timestamp: Timestamp to use for last_seen (defaults to now)
Returns:
List of JSONPathType objects with all discovered paths and types
"""
if timestamp is None:
timestamp = datetime.datetime.now()
@@ -233,9 +268,11 @@ def _parse_json_bodies_and_extract_paths(
parsed = json.loads(json_body)
_extract_json_paths(parsed, "", all_path_types, level=0)
except (json.JSONDecodeError, TypeError):
# Skip invalid JSON
continue
# Each path can have multiple types -> one JSONPathType per type
# Convert to list of JSONPathType objects
# Each path can have multiple types, so we create one JSONPathType per type
path_type_objects: list[JSONPathType] = []
for path, types_set in all_path_types.items():
for type_str in types_set:
@@ -248,34 +285,64 @@ def _parse_json_bodies_and_extract_paths(
def export_json_types(
clickhouse: types.TestContainerClickhouse,
) -> Generator[Callable[[list[JSONPathType] | list[str] | list[Any]], None], Any]:
"""Write JSON path/type metadata the way the real metadataexporter would.
"""
Fixture for exporting JSON type metadata to the path_types table.
This is a simpler version of metadataexporter for test fixtures.
Accepts JSONPathType objects (manual specification), raw JSON body strings,
or Logs objects (paths auto-extracted from the JSON body).
The function can accept:
1. List of JSONPathType objects (manual specification)
2. List of JSON body strings (auto-extract paths)
3. List of Logs objects (extract from body_json field)
Usage examples:
# Manual specification
export_json_types([
JSONPathType(field_name="user.name", field_data_type="string"),
JSONPathType(field_name="user.age", field_data_type="int64"),
])
# Auto-extract from JSON strings
export_json_types([
'{"user": {"name": "alice", "age": 25}}',
'{"user": {"name": "bob", "age": 30}}',
])
# Auto-extract from Logs objects
export_json_types(logs_list)
"""
def _export_json_types(
data: list[JSONPathType] | list[str] | list[Any], # List[Logs] but avoiding circular import
) -> None:
"""
Export JSON type metadata to signoz_metadata.distributed_field_keys table.
This table stores signal, context, path, and type information for body JSON fields.
"""
path_types: list[JSONPathType] = []
if len(data) == 0:
return
# Determine input type and convert to JSONPathType list
first_item = data[0]
if isinstance(first_item, JSONPathType):
# Already JSONPathType objects
path_types = data # type: ignore
elif isinstance(first_item, str):
# List of JSON strings - parse and extract paths
path_types = _parse_json_bodies_and_extract_paths(data) # type: ignore
else:
# Assume it's a list of Logs objects - extract body_v2
json_bodies: list[str] = []
for log in data: # type: ignore
# Try to get body_v2 attribute
if hasattr(log, "body_v2") and log.body_v2:
json_bodies.append(log.body_v2)
elif hasattr(log, "body") and log.body:
# Fallback to body if body_v2 not available
try:
# Try to parse as JSON
json.loads(log.body)
json_bodies.append(log.body)
except (json.JSONDecodeError, TypeError):
@@ -302,6 +369,7 @@ def export_json_types(
yield _export_json_types
# Cleanup - truncate the local table after tests (following pattern from logs fixture)
clickhouse.conn.query(f"TRUNCATE TABLE signoz_metadata.field_keys ON CLUSTER '{clickhouse.env['SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER']}' SYNC")

View File

@@ -109,6 +109,9 @@ def keeper(
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.TestContainerDocker:
"""
Package-scoped fixture for ClickHouse Keeper TestContainer.
"""
return create_clickhouse_keeper(
tmpfs=tmpfs,
network=network,

View File

@@ -19,6 +19,9 @@ def idp(
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.TestContainerIDP:
"""
Package-scoped fixture for running an idp for SSO/SAML
"""
def create() -> types.TestContainerIDP:
container = KeycloakContainer(

View File

@@ -311,6 +311,7 @@ class Logs(ABC):
self.attribute_keys.append(LogsResourceOrAttributeKeys(name="severity_number", datatype="float64"))
def _get_severity_number(self, severity_text: str) -> np.uint8:
"""Convert severity text to numeric value"""
severity_map = {
"TRACE": 1,
"DEBUG": 5,
@@ -323,6 +324,7 @@ class Logs(ABC):
return np.uint8(severity_map.get(severity_text.upper(), 9)) # Default to INFO
def np_arr(self) -> np.array:
"""Return log data as numpy array for database insertion"""
return np.array(
[
self.ts_bucket_start,
@@ -354,6 +356,7 @@ class Logs(ABC):
cls,
data: dict,
) -> "Logs":
"""Create a Logs instance from a dict."""
# parse timestamp from iso format
timestamp = parse_timestamp(data["timestamp"])
return cls(

View File

@@ -374,7 +374,6 @@ class Metrics(ABC):
file_path: str,
base_time: datetime.datetime | None = None,
metric_name_override: str | None = None,
label_substitutions: dict[str, str] | None = None,
) -> list["Metrics"]:
"""
Load metrics from a JSONL file.
@@ -386,9 +385,6 @@ class Metrics(ABC):
base_time: If provided, all timestamps are shifted so the earliest
timestamp in the file maps to base_time
metric_name_override: If provided, overrides metric_name for all metrics
label_substitutions: If provided, any label whose value equals a key is
rewritten to that key's value (placeholder substitution,
e.g. {"__START_TIME__": start_time.isoformat()})
"""
data_list = []
with open(file_path, encoding="utf-8") as f:
@@ -396,13 +392,7 @@ class Metrics(ABC):
line = line.strip()
if not line:
continue
data = json.loads(line)
if label_substitutions:
labels = data.get("labels", {})
for key, value in labels.items():
if value in label_substitutions:
labels[key] = label_substitutions[value]
data_list.append(data)
data_list.append(json.loads(line))
if not data_list:
return []

View File

@@ -92,6 +92,9 @@ def migrator(
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.Operation:
"""
Package-scoped fixture for running schema migrations.
"""
return create_migrator(
network=network,
clickhouse=clickhouse,

View File

@@ -13,6 +13,9 @@ logger = setup_logger(__name__)
@pytest.fixture(name="network", scope="package")
def network(request: pytest.FixtureRequest, pytestconfig: pytest.Config) -> types.Network:
"""
Package-Scoped fixture for creating a network
"""
def create() -> types.Network:
nw = Network()

View File

@@ -13,6 +13,9 @@ logger = setup_logger(__name__)
@pytest.fixture(name="postgres", scope="package")
def postgres(network: Network, request: pytest.FixtureRequest, pytestconfig: pytest.Config) -> types.TestContainerSQL:
"""
Package-scoped fixture for PostgreSQL TestContainer.
"""
def create() -> types.TestContainerSQL:
version = request.config.getoption("--postgres-version")

44
tests/fixtures/promapi.py vendored Normal file
View File

@@ -0,0 +1,44 @@
"""Client helpers for the /prometheus/api/v1 endpoints."""
import math
import requests
from fixtures import types
SPECIALS = {"NaN": math.nan, "Inf": math.inf, "+Inf": math.inf, "-Inf": -math.inf}
QUERY_TIMEOUT = 30
def prom_api_get(signoz: types.SigNoz, token: str, path: str, params: dict) -> requests.Response:
return requests.get(
signoz.self.host_configs["8080"].get(path),
params=params,
timeout=QUERY_TIMEOUT,
headers={"authorization": f"Bearer {token}"},
)
def prom_api_value(v: str) -> float:
"""Prometheus API sample values are strings, including "NaN" and "+Inf"."""
if v in SPECIALS:
return SPECIALS[v]
return float(v)
def series_from_prom_result(result_type: str, result) -> dict[tuple, dict[int, float]]:
"""Flattens a matrix/vector/scalar result into
{sorted-labels tuple: {unix_ms: value}}."""
out: dict[tuple, dict[int, float]] = {}
if result_type == "matrix":
for series in result:
points = {round(float(ts) * 1000): prom_api_value(v) for ts, v in series.get("values") or []}
out[tuple(sorted((series.get("metric") or {}).items()))] = points
elif result_type == "vector":
for series in result:
ts, v = series["value"]
out[tuple(sorted((series.get("metric") or {}).items()))] = {round(float(ts) * 1000): prom_api_value(v)}
elif result_type == "scalar":
ts, v = result
out[()] = {round(float(ts) * 1000): prom_api_value(v)}
return out

112
tests/fixtures/promqltestcorpus.py vendored Normal file
View File

@@ -0,0 +1,112 @@
"""Shared helpers for suites that replay the frozen promqltest corpus
(tests/integration/testdata/promqltestcorpus/corpus.json)."""
import json
import math
import os
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
import pytest
from fixtures.metrics import Metrics
TESTDATA_DIR = os.path.join(os.path.dirname(__file__), "..", "integration", "testdata", "promqltestcorpus")
CORPUS_FILE = os.path.join(TESTDATA_DIR, "corpus.json")
ISOLATION_GAP_MS = 2 * 3600 * 1000
SPECIALS = {"NaN": math.nan, "Inf": math.inf, "+Inf": math.inf, "-Inf": -math.inf}
def decode_corpus_value(v: float | str) -> float:
if isinstance(v, str):
return SPECIALS[v]
return float(v)
def values_close(a: float, b: float) -> bool:
"""Expected corpus values carry the v5 API's rounding (>=1: three decimal
places; <1: three significant digits). One rounding quantum covers both a
raw-vs-rounded comparison and a boundary that rounds either way."""
if math.isnan(a) or math.isnan(b):
return math.isnan(a) and math.isnan(b)
if math.isinf(a) or math.isinf(b):
return a == b
if a == b:
return True
scale = max(abs(a), abs(b))
if scale >= 1:
quantum = max(1e-3, scale * 1e-9)
else:
quantum = 10 ** (math.floor(math.log10(scale)) - 2)
return abs(a - b) <= quantum + 1e-12
def labelset(labels: dict[str, str]) -> tuple:
return tuple(sorted(labels.items()))
def ledger(filename: str) -> dict[str, str]:
path = os.path.join(TESTDATA_DIR, filename)
if not os.path.exists(path):
return {}
with open(path, encoding="utf-8") as f:
return json.load(f)["divergences"]
@pytest.fixture(name="ingest_promqltest_corpus")
def ingest_promqltest_corpus(insert_metrics: Callable[[list[Metrics]], None]) -> Callable[[], tuple[dict, dict[int, int]]]:
"""Yields a callable that loads the corpus, lays its datasets end to end
on the timeline, ingests every sample, and returns (corpus, dataset base
timestamps).
Dataset bases are hour-aligned: registration rows are hour-bucketed, so
behavior depends on where samples fall relative to hour boundaries, and
exact known-divergences enforcement needs identical placement every run.
Datasets sit on disjoint windows (2h gaps, far beyond the 5m lookback) so
one bulk ingest serves every case without cross-talk."""
def ingest() -> tuple[dict, dict[int, int]]:
with open(CORPUS_FILE, encoding="utf-8") as f:
corpus = json.load(f)
cases_by_dataset: dict[int, list[dict]] = {}
for case in corpus["cases"]:
cases_by_dataset.setdefault(case["dataset"], []).append(case)
spans = {}
for ds in corpus["datasets"]:
sample_max = max((s["samples"][-1][0] for s in ds["series"] if s["samples"]), default=0)
case_max = max((c["end_ms"] for c in cases_by_dataset.get(ds["id"], [])), default=0)
spans[ds["id"]] = max(sample_max, case_max) + corpus["meta"]["lookback_ms"]
hour_ms = 3_600_000
advances = {ds["id"]: -(-(spans[ds["id"]] + ISOLATION_GAP_MS) // hour_ms) * hour_ms for ds in corpus["datasets"]}
total = sum(advances.values())
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
cursor = (int((now - timedelta(hours=1)).timestamp() * 1000) - total) // hour_ms * hour_ms
bases: dict[int, int] = {}
metrics: list[Metrics] = []
for ds in corpus["datasets"]:
bases[ds["id"]] = cursor
for series in ds["series"]:
labels = dict(series["labels"])
metric_name = labels.pop("__name__")
for off_ms, raw in series["samples"]:
stale = raw == "stale"
metrics.append(
Metrics(
metric_name=metric_name,
labels=labels,
timestamp=datetime.fromtimestamp((cursor + off_ms) / 1000, tz=UTC),
value=0.0 if stale else decode_corpus_value(raw),
flags=1 if stale else 0,
)
)
cursor += advances[ds["id"]]
insert_metrics(metrics)
return corpus, bases
return ingest

View File

@@ -704,7 +704,6 @@ def build_raw_query(
order: list[dict] | None = None,
limit: int | None = None,
filter_expression: str | None = None,
select_fields: list[dict] | None = None,
step_interval: int = DEFAULT_STEP_INTERVAL,
disabled: bool = False,
) -> dict:
@@ -724,9 +723,6 @@ def build_raw_query(
if filter_expression:
spec["filter"] = {"expression": filter_expression}
if select_fields:
spec["selectFields"] = select_fields
return {"type": "builder_query", "spec": spec}
@@ -1109,47 +1105,3 @@ def make_scalar_query_request(
"formatOptions": {"formatTableResultForUI": True, "fillGaps": False},
},
)
def run_query_case(signoz: types.SigNoz, token: str, now: datetime, case: dict[str, Any]) -> None:
start_ms = case.get("startMs", int((now - timedelta(seconds=10)).timestamp() * 1000))
end_ms = case.get("endMs", int(now.timestamp() * 1000))
if case["requestType"] == "raw":
query = build_raw_query(
name=case["name"],
signal="logs",
filter_expression=case.get("expression"),
order=case.get("order") or [build_order_by("timestamp", "desc")],
limit=case.get("limit", 100),
step_interval=case.get("stepInterval") or 60,
)
else:
aggregation = case.get("aggregation")
if aggregation and not isinstance(aggregation, list):
aggregations = [build_aggregation(aggregation)]
elif aggregation:
aggregations = aggregation
else:
aggregations = []
query = build_scalar_query(
name=case["name"],
signal="logs",
aggregations=aggregations,
group_by=case.get("groupBy"),
order=case.get("order"),
limit=case.get("limit", 100),
filter_expression=case.get("expression"),
step_interval=case.get("stepInterval") or 60,
)
response = make_query_request(
signoz=signoz,
token=token,
start_ms=start_ms,
end_ms=end_ms,
queries=[query],
request_type=case["requestType"],
)
assert response.status_code == 200, f"HTTP {response.status_code} for case '{case['name']}': {response.text}"
assert case["validate"](response), f"Validation failed for case '{case['name']}': {response.json()}"

View File

@@ -1,3 +1,8 @@
"""
Trace builders for the querierai suite. Every builder pins its spans a few seconds
before the given `now` so `query_window(now)` covers them.
"""
from datetime import datetime, timedelta
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode

View File

@@ -1,124 +0,0 @@
"""Seed data for the queriercommon keyless-semantics tests.
Three identities exist in every signal. GOLD and SILVER carry the test keys.
NONE carries no key at all. The tests assert which identities a filter
returns, so the membership of NONE is the point of every case.
The attribute names are outside every semantic-convention family, so the
seeded data pins base behavior with any semconv overlay state.
"""
from collections.abc import Callable, Generator
from datetime import UTC, datetime, timedelta
import pytest
from fixtures.logs import Logs
from fixtures.metrics import Metrics
from fixtures.querier import aligned_epoch
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode
PREFIX = "keyless-sem"
STRING_KEY = "tenant.tier"
NUMBER_KEY = "retry.count"
METRIC_NAME = "keyless_semantics_gauge"
METRIC_LABEL = "tenant_tier"
# Row identities, keyed by the value of the string key that each row carries.
GOLD = f"{PREFIX}-gold"
SILVER = f"{PREFIX}-silver"
NONE = f"{PREFIX}-none" # carries no string key and no number key
# (identity, string-key value, number-key value, insert offset)
_ROWS = [
(GOLD, "gold", 0, timedelta(seconds=3)),
(SILVER, "silver", 5, timedelta(seconds=2)),
(NONE, None, None, timedelta(seconds=1)),
]
def _resources(identity: str, tier: str | None) -> dict:
base = {"service.name": identity}
if tier is not None:
base[STRING_KEY] = tier
return base
def _attributes(tier: str | None, retries: int | None) -> dict:
attrs: dict = {}
if tier is not None:
attrs[STRING_KEY] = tier
if retries is not None:
attrs[NUMBER_KEY] = retries
return attrs
@pytest.fixture(name="keyless_rows", scope="function")
def keyless_rows(
insert_logs: Callable[[list[Logs]], None],
insert_traces: Callable[[list[Traces]], None],
) -> Generator[datetime]:
"""Inserts one span and one log per identity: GOLD (string "gold",
number 0), SILVER (string "silver", number 5), and NONE (no keys).
Yields the base timestamp. Span name and log body are the identity."""
now = datetime.now(tz=UTC).replace(microsecond=0) - timedelta(minutes=1)
insert_traces(
[
Traces(
timestamp=now - offset,
duration=timedelta(milliseconds=10),
trace_id=TraceIdGenerator.trace_id(),
span_id=TraceIdGenerator.span_id(),
name=identity,
kind=TracesKind.SPAN_KIND_SERVER,
status_code=TracesStatusCode.STATUS_CODE_OK,
resources=_resources(identity, tier),
attributes=_attributes(tier, retries),
)
for identity, tier, retries, offset in _ROWS
]
)
insert_logs(
[
Logs(
timestamp=now - offset,
body=identity,
resources=_resources(identity, tier),
attributes=_attributes(tier, retries),
)
for identity, tier, retries, offset in _ROWS
]
)
yield now
@pytest.fixture(name="keyless_series", scope="function")
def keyless_series(insert_metrics: Callable[[list[Metrics]], None]) -> Generator[tuple[int, int]]:
"""Inserts three gauge series: GOLD and SILVER carry the metric label,
NONE does not. The `service` label is the identity. Yields the
(start, end) epoch-second window that covers the points."""
start = aligned_epoch(timedelta(minutes=30))
points = 5
def labels(identity: str, tier: str | None) -> dict:
base = {"service": identity}
if tier is not None:
base[METRIC_LABEL] = tier
return base
insert_metrics(
[
Metrics(
metric_name=METRIC_NAME,
labels=labels(identity, tier),
timestamp=datetime.fromtimestamp(start + minute * 60, tz=UTC),
value=10.0,
type_="Gauge",
is_monotonic=False,
)
for identity, tier in ((GOLD, "gold"), (SILVER, "silver"), (NONE, None))
for minute in range(points)
]
)
yield start, start + points * 60

View File

@@ -19,14 +19,17 @@ def teardown(request: pytest.FixtureRequest) -> bool:
def get_cached_resource(pytestconfig: pytest.Config, key: str):
"""Get a resource from pytest cache by key."""
return pytestconfig.cache.get(key, None)
def set_cached_resource(pytestconfig: pytest.Config, key: str, value):
"""Set a resource in pytest cache by key."""
pytestconfig.cache.set(key, value)
def remove_cached_resource(pytestconfig: pytest.Config, key: str):
"""Remove a resource from pytest cache by key (set to None)."""
pytestconfig.cache.set(key, None)

View File

@@ -1,3 +1,5 @@
"""Fixtures and helpers for role tests."""
import json
from collections.abc import Callable
from http import HTTPStatus

View File

@@ -1,3 +1,11 @@
"""Golden dataset fixture — seeds OTel-demo-shaped metrics, traces, and
logs into ClickHouse via the seeder on every test_setup invocation.
Timestamps are rebased to `now` so panels with default time windows
always find data. To refresh the dataset shape on disk, run
`uv run python -m fixtures.seed_golden_dataset regenerate`.
"""
from __future__ import annotations
import datetime

View File

@@ -1,3 +1,5 @@
"""Fixtures and helpers for service account tests."""
from http import HTTPStatus
import requests

View File

@@ -225,6 +225,9 @@ def signoz( # pylint: disable=too-many-arguments,too-many-positional-arguments
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.SigNoz:
"""
Package-scoped fixture for setting up SigNoz.
"""
return create_signoz(
network=network,
zeus=zeus,

View File

@@ -7,6 +7,9 @@ from fixtures import types
def sqlstore(
request: pytest.FixtureRequest,
) -> types.TestContainerSQL:
"""
Packaged-scoped fixture for creating sql store.
"""
provider = request.config.getoption("--sqlstore-provider")
if provider == "postgres":

View File

@@ -16,6 +16,9 @@ def sqlite(
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.TestContainerSQL:
"""
Package-scoped fixture for SQLite.
"""
def create() -> types.TestContainerSQL:
tmpdir = tmpfs("sqlite")

View File

@@ -1,3 +1,9 @@
"""Shared helpers for the third-party (external) API monitoring domain list.
A translator over v5 builder queries that answers with a UI-formatted scalar table, so the
response is read by column rather than by series.
"""
from typing import Any
import requests
@@ -43,9 +49,7 @@ def make_third_party_apis_request(
def scalar_result(response: requests.Response) -> dict:
"""The single scalar table from a third-party-apis response. The endpoint is a
translator over v5 builder queries answering with a UI-formatted scalar table,
so the response is read by column rather than by series."""
"""The single scalar table from a third-party-apis response."""
return response.json()["data"]["data"]["results"][0]

View File

@@ -6,6 +6,9 @@ import isodate
# parses the given timestamp string from ISO format to datetime.datetime
def parse_timestamp(ts_str: str) -> datetime.datetime:
"""
Parse a timestamp string from ISO format.
"""
if ts_str.endswith("Z"):
ts_str = ts_str[:-1] + "+00:00"
return datetime.datetime.fromisoformat(ts_str)
@@ -13,6 +16,9 @@ def parse_timestamp(ts_str: str) -> datetime.datetime:
# parses the given duration to datetime.timedelta
def parse_duration(duration: Any) -> datetime.timedelta:
"""
Parse a duration string from ISO format.
"""
# if it's string then parse it as iso format
if isinstance(duration, str):
return isodate.parse_duration(duration)

View File

@@ -625,6 +625,7 @@ class Traces(ABC):
self.response_status_code = str_value
def np_arr(self) -> np.array:
"""Return span data as numpy array for database insertion"""
return np.array(
[
self.ts_bucket_start,
@@ -668,6 +669,7 @@ class Traces(ABC):
cls,
data: dict,
) -> "Traces":
"""Create a Traces instance from a dict."""
# parse timestamp from iso format
timestamp = parse_timestamp(data["timestamp"])
duration = parse_duration(data.get("duration", "PT1S"))

View File

@@ -88,15 +88,3 @@
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-ss-uid","k8s.pod.name":"nd-ss-pod","k8s.statefulset.name":"nd-ss","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-ss-uid","k8s.pod.name":"nd-ss-pod","k8s.statefulset.name":"nd-ss","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-ss-uid","k8s.pod.name":"nd-ss-pod","k8s.statefulset.name":"nd-ss","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-uid", "k8s.pod.name": "nd-ds-p1", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "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.phase", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "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": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "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": "nd-ds-p1-uid", "k8s.pod.name": "nd-ds-p1", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "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.phase", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "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": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "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": "nd-ds-p1-uid", "k8s.pod.name": "nd-ds-p1", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "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.phase", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "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": "nd-ds-p1-clbo-uid", "k8s.pod.name": "nd-ds-p1-clbo", "k8s.daemonset.name": "nd-ds", "k8s.namespace.name": "ns-nd", "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}

View File

@@ -94,15 +94,3 @@
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nj-ss-uid","k8s.pod.name":"nj-ss-pod","k8s.statefulset.name":"nj-ss","k8s.namespace.name":"ns-nj","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nj-ss-uid","k8s.pod.name":"nj-ss-pod","k8s.statefulset.name":"nj-ss","k8s.namespace.name":"ns-nj","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nj-ss-uid","k8s.pod.name":"nj-ss-pod","k8s.statefulset.name":"nj-ss","k8s.namespace.name":"ns-nj","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-uid", "k8s.pod.name": "nj-job-p1", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "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.phase", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "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": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "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": "nj-job-p1-uid", "k8s.pod.name": "nj-job-p1", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "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.phase", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "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": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "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": "nj-job-p1-uid", "k8s.pod.name": "nj-job-p1", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "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.phase", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "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": "nj-job-p1-clbo-uid", "k8s.pod.name": "nj-job-p1-clbo", "k8s.job.name": "nj-job", "k8s.namespace.name": "ns-nj", "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}

View File

@@ -1,36 +1,36 @@
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.deployment.name":"gb-dep-shared","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.deployment.name":"gb-dep-b3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.deployment.name":"gb-dep-b4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-1-uid","k8s.pod.name":"pod-gb-ns-1","k8s.namespace.name":"gb-ns-1","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-2-uid","k8s.pod.name":"pod-gb-ns-2","k8s.namespace.name":"gb-ns-2","k8s.cluster.name":"gb-cluster-a"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-3-uid","k8s.pod.name":"pod-gb-ns-3","k8s.namespace.name":"gb-ns-3","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.memory.working_set","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":100000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"pod-gb-ns-4-uid","k8s.pod.name":"pod-gb-ns-4","k8s.namespace.name":"gb-ns-4","k8s.cluster.name":"gb-cluster-b"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}

View File

@@ -31,48 +31,3 @@
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"carrier-pod-uid","k8s.pod.name":"carrier-pod","k8s.namespace.name":"carrier-ns","k8s.node.name":"carrier-phantom-host","k8s.cluster.name":"carrier-cluster"},"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":"carrier-pod-uid","k8s.pod.name":"carrier-pod","k8s.namespace.name":"carrier-ns","k8s.node.name":"carrier-phantom-host","k8s.cluster.name":"carrier-cluster"},"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":"carrier-pod-uid","k8s.pod.name":"carrier-pod","k8s.namespace.name":"carrier-ns","k8s.node.name":"carrier-phantom-host","k8s.cluster.name":"carrier-cluster"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.8, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "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.node.condition_ready", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "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.node.condition_ready", "labels": {"k8s.node.name": "ready-n2", "k8s.node.uid": "ready-n2-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "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.node.condition_ready", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "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.node.condition_ready", "labels": {"k8s.node.name": "ready-n3", "k8s.node.uid": "ready-n3-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.cpu.usage", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.6, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_cpu", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 4.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.memory.working_set", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.allocatable_memory", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 8000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.node.condition_ready", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "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.node.condition_ready", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "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.node.condition_ready", "labels": {"k8s.node.name": "ready-n4", "k8s.node.uid": "ready-n4-uid", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}

View File

@@ -145,63 +145,3 @@
{"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}
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.7, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.7, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.7, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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": "clbo-a-uid", "k8s.pod.name": "clbo-a", "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.cpu.usage", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.cpu_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory_request_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.memory_limit_utilization", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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": "clbo-b-uid", "k8s.pod.name": "clbo-b", "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}

View File

@@ -67,15 +67,3 @@
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-dep-uid","k8s.pod.name":"nd-dep-pod","k8s.deployment.name":"nd-dep","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-dep-uid","k8s.pod.name":"nd-dep-pod","k8s.deployment.name":"nd-dep","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.phase","labels":{"k8s.pod.uid":"nd-dep-uid","k8s.pod.name":"nd-dep-pod","k8s.deployment.name":"nd-dep","k8s.namespace.name":"ns-nd","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-uid", "k8s.pod.name": "ns-ss-p1", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "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.phase", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "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": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "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": "ns-ss-p1-uid", "k8s.pod.name": "ns-ss-p1", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "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.phase", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "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": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "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": "ns-ss-p1-uid", "k8s.pod.name": "ns-ss-p1", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "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.phase", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "k8s.cluster.name": "cluster-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.pod.status_reason", "labels": {"k8s.pod.uid": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "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": "ns-ss-p1-clbo-uid", "k8s.pod.name": "ns-ss-p1-clbo", "k8s.statefulset.name": "ns-ss", "k8s.namespace.name": "ns-nd", "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}

View File

@@ -0,0 +1,12 @@
{
"note": "Divergences of the /prometheus/api/v1 endpoints, served by the clickhousev2 provider, from the upstream reference engine. 01_prometheus_api_corpus.py enforces this set exactly in both directions. All current entries are the Kahan class recorded in known_divergences_v2.json: the engine sums with Kahan compensation and an overflow-free incremental mean, ClickHouse's aggregates are naive. Only the [instant-coarse] variants appear here: they are range-encoded, so they serve transpiled; the [base] instant evals go through /prometheus/api/v1/query on the exact engine path.",
"divergences": {
"aggregators.test:651[instant-coarse]": "avg over near-max-float64 values: avgForEach overflows to +Inf where the engine's incremental mean does not",
"aggregators.test:654[instant-coarse]": "avg over near-min-float64 values: avgForEach overflows to -Inf",
"aggregators.test:687[instant-coarse]": "sum over {1e100, -1e100, small}: naive summation cancels to 0 where the engine's Kahan sum keeps 10",
"aggregators.test:695[instant-coarse]": "avg over {1e100, -1e100, small}: same cancellation divided by count",
"functions.test:1084[instant-coarse]": "sum_over_time over a ±1e100 window: the disjoint coarse-step form's arraySum cancels to 0",
"functions.test:1087[instant-coarse]": "avg_over_time, same window and cancellation as functions.test:1084",
"functions.test:1149[instant-coarse]": "avg_over_time over ±2.258e220 samples: naive slide summation leaves a ~1e202 residue where the engine cancels to 0"
}
}

View File

@@ -20,6 +20,9 @@ def test_webhook_notification_channel(
make_http_mocks: Callable[[types.TestContainerDocker, list[Mapping]], None],
create_webhook_notification_channel: Callable[[str, str, dict, bool], str],
) -> None:
"""
Tests the creation and delivery of test alerts on the created notification channel
"""
logger.info("Setting up notification channel")
# Prepare notification channel name and webhook endpoint

View File

@@ -176,6 +176,7 @@ def test_create_invalid_role_mapping(
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
"""Test that invalid role mappings are rejected."""
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Create domain with invalid defaultRole

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