Compare commits

..

8 Commits

Author SHA1 Message Date
Nagesh Bansal
67418bb132 docs: fix the signoz docs troubleshooting guide url 2026-06-25 07:42:57 +05:30
Nagesh Bansal
2e2517449d docs(deploy): title migration guide for both install script and deploy 2026-06-25 06:52:36 +05:30
Nagesh Bansal
0150c55361 docs(deploy): restructure migration guide and add Docker Swarm 2026-06-25 06:38:48 +05:30
Abir Roy
a609a4044c fix(ui): resolve monaco find widget clipping and flickering (#11826)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-06-24 20:11:11 +00:00
Nikhil Mantri
f78d98ea71 feat(metrics-explorer): move metric_name from path param to query param (#11745)
* chore: metricName to post body for POST /api/v2/metrics/{metric_name}/metadata

* chore: metricName to query param for GET /api/v2/metrics/{metric_name}/metadata

* chore: added metricName in api get metric attributes

* chore: highlights api modified

* chore: alerts api modified

* chore: dashboards api modified

* chore: description added for metric_name query params

* feat(metrics-explorer): integrate metricName query/body API change in frontend (#11818)

* feat(metrics-explorer): integrate metricName query/body API change in frontend

The metrics-explorer endpoints moved metric_name off the URL path: the
five GETs (attributes, metadata, highlights, alerts, dashboards) now take
a required `metricName` query param, and POST /metadata reads metricName
from the request body.

- Regenerate the orval client from the updated openapi spec, so the GET
  helpers build `/api/v2/metrics/<op>?metricName=...` (URL-encoded, so
  slashed cloud metric names work) and updateMetricMetadata posts to
  `/api/v2/metrics/metadata` with metricName in the body.
- Collapse the useGetMetricAttributes call to the single merged params
  object (metricName + start/end).
- Drop the now-removed pathParams wrapper from both updateMetricMetadata
  call sites; the payload builders already include metricName in the body.
- Update the Metadata test to assert metricName inside the request body.

* revert(metrics-explorer): drop slashed-metric-name band-aid guards

These two defensive guards were added as temporary workarounds for the
metric_name-with-slash bug (SigNoz/signoz#11527, #11528), which returned
200 + HTML instead of JSON. The root cause is fixed by moving metricName
to a query/body param, so the band-aids are no longer needed and revert
to the original intended code.

- MetricDetails.tsx: `!metricMetadataResponse?.data` -> `!metricMetadataResponse`
- AllAttributes.tsx: `?.data?.attributes` -> `?.data.attributes`

* chore: added description for metricName query params

---------

Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-06-24 19:40:09 +00:00
Ashwin Bhatkal
f60e5039be feat(dashboard-v2): toolbar repositioning, JSON editor & expandable variables bar (#11837)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(dashboard-v2): json editor drawer to edit dashboard as raw JSON

Right-side drawer with a Monaco JSON editor (reusing the import-JSON theme),
a Format/Copy/Download/Reset toolbar, live JSON validation, and Apply via the
full-document updateDashboardV2 endpoint. Cmd/Ctrl+Enter applies; Esc closes.

* feat(dashboard-v2): grouped actions menu with clone, new section & edit-as-JSON

Regroup the actions dropdown into labelled Dashboard/Data/Layout groups, add
Clone dashboard (cloneDashboardV2 + navigate) and New section (useAddSection),
and surface an Edit as JSON button that opens the JSON editor drawer. The menu
trigger is a labelled "Actions" button with a 9-grid icon; the time selector
moves out of the actions row to the toolbar's second row.

* feat(dashboard-v2): description-on-hover and space-aware tag overflow

Collapse the dashboard description behind an info icon shown on hover, and move
tags inline after the title where they restrict to the available width and
collapse the remainder into a +N badge (reusing the alerts badge measuring).

* refactor(dashboard-v2): two-row toolbar with a floated, expandable variables bar

Second toolbar row floats the time-range selector top-right; the variables bar
flows beside it, collapsing to a single line with an inline +N trigger that hugs
the last visible pill. Expanding clears the float so the pills pack full-width
on the lines beneath the time selector (no stair-stepping). Overflow pills are
display:none but stay mounted (widths cached) so auto-selection and option
fetching keep driving the panels. Also centre the variable info icon, give the
pills a visible --l3-border (and drop the single-select's stray inner border so
it matches), and replace the toolbar's fuzzy drop shadow with a token hairline.

* feat(dashboard-v2): section title modal & scroll to the new section

New section now opens a title-entry modal instead of inserting a default-named
section, and the view scrolls the freshly created section into view once the
refetch renders it. Generalise the rename modal into a shared SectionTitleModal
reused by both create and rename.

* test(dashboard-v2): cover the JSON editor hook and drawer

useJsonEditor: seeding, live validation, format/reset, dirty tracking, apply
(no-op when clean/invalid, PUTs the narrowed body, error handling) and re-seed
on re-open. JsonEditorDrawer: toolbar/footer wiring, validation text, Apply
enablement, editor changes and Cmd/Ctrl+Enter — with Monaco and the hook mocked.

* refactor(dashboard-v2): extract generic useInlineOverflowCount hook

Address review on the variables bar: generalise the single-line overflow
measurement into hooks/useInlineOverflowCount (container of data-overflow-item
children, with gap/reserveWidth/enabled options) so it's reusable elsewhere, and
clarify the internal variable names (container/itemWidths/availableWidth/etc.).

* refactor(dashboard-v2): use descriptive names in useInlineOverflowCount

Replace single-letter/abbreviated locals (el, i, w, width/widths) with
itemElement/index/itemWidth/cachedWidth(s) inside the measure loop.
2026-06-24 12:54:34 +00:00
Vikrant Gupta
a483ef81a4 feat(authz): add transaction groups JSON schema (#11827)
* feat(authz): add transaction group schema and validations

* fix(authz): drop constant errorFormat param from wrapValidationError

unparam flagged wrapValidationError's errorFormat parameter since all
call sites passed the same "%s: %s". Inline the format and trim the
argument at each call site. No behavior change.

* feat(authz): better error handling

* chore(authz): suffix generated web settings schema with .schema.json

Rename webSettings.json to webSettings.schema.json to follow the JSON
Schema file-naming convention and match transactionGroups.schema.json.
Updates the generator output path, the json2ts input + banner in
package.json, and the generated banner comment.

* feat(authz): add schema titles
2026-06-24 11:27:19 +00:00
Abhi kumar
b9c107a851 fix(dashboards-v2): stop infinite render loop on dashboards with no variable selections (#11841)
selectVariableValues returned an inline `{}` fallback whenever a dashboard had
no stored selections. Zustand reads selectors through useSyncExternalStore,
which compares snapshots with Object.is, so a fresh object every call reads as a
perpetually-changed snapshot and React re-renders without end ("Maximum update
depth exceeded").

This surfaced specifically on fresh/empty dashboards: when a dashboard has
variables, the seeding effect in useVariableSelection populates the store with a
stable object and the loop never starts; with no variables that effect
early-returns, the entry stays undefined, and the selector mints a new `{}` on
every render. VariablesBar renders null in that case, but its hook still
subscribes, so the loop fires anyway.

Return a single module-level empty map so the snapshot is referentially stable.
2026-06-24 10:16:48 +00:00
78 changed files with 3178 additions and 1375 deletions

View File

@@ -140,3 +140,20 @@ jobs:
run: |
go run cmd/enterprise/*.go generate config web-settings
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in web settings schema. Run go run cmd/enterprise/*.go generate config web-settings locally and commit."; exit 1)
transaction-groups:
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: generate-transaction-groups
run: |
go run cmd/enterprise/*.go generate config transaction-groups
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in transaction groups schema. Run go run cmd/enterprise/*.go generate config transaction-groups locally and commit."; exit 1)

View File

@@ -6,12 +6,15 @@ import (
"reflect"
"strings"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/web"
"github.com/spf13/cobra"
"github.com/swaggest/jsonschema-go"
)
const webSettingsSchemaPath = "docs/config/web-settings.json"
const webSettingsSchemaPath = "frontend/src/schemas/generated/webSettings.schema.json"
const transactionGroupsSchemaPath = "frontend/src/schemas/generated/transactionGroups.schema.json"
func registerGenerateConfig(parentCmd *cobra.Command) {
configCmd := &cobra.Command{
@@ -27,6 +30,14 @@ func registerGenerateConfig(parentCmd *cobra.Command) {
},
})
configCmd.AddCommand(&cobra.Command{
Use: "transaction-groups",
Short: "Generate JSON Schema for transaction groups",
RunE: func(currCmd *cobra.Command, args []string) error {
return generateTransactionGroups()
},
})
parentCmd.AddCommand(configCmd)
}
@@ -52,6 +63,7 @@ func generateWebSettings() error {
return err
}
schema.WithTitle("WebSettings")
data, err := json.MarshalIndent(schema, "", " ")
if err != nil {
return err
@@ -59,3 +71,31 @@ func generateWebSettings() error {
return os.WriteFile(webSettingsSchemaPath, append(data, '\n'), 0o600)
}
func generateTransactionGroups() error {
falseVal := false
noAdditional := jsonschema.SchemaOrBool{TypeBoolean: &falseVal}
reflector := jsonschema.Reflector{}
reflector.DefaultOptions = append(reflector.DefaultOptions,
jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (bool, error) {
if params.Value.Kind() == reflect.Struct {
params.Schema.AdditionalProperties = &noAdditional
}
return false, nil
}),
)
schema, err := reflector.Reflect(authtypes.TransactionGroups{})
if err != nil {
return err
}
schema.WithTitle("TransactionGroups")
data, err := json.MarshalIndent(schema, "", " ")
if err != nil {
return err
}
return os.WriteFile(transactionGroupsSchemaPath, append(data, '\n'), 0o600)
}

View File

@@ -1,48 +1,76 @@
# Migrating from the install script to Foundry
# Migrating from the install script and `deploy/` to Foundry
The install script (`install.sh`) and the bundled Compose and Swarm files
under `deploy/` are deprecated in favor of [Foundry][foundry], the supported
way to install and manage SigNoz. This guide moves an existing Docker Compose
or Docker Swarm deployment to Foundry and reattaches your existing volumes, so
your data is preserved.
> [!IMPORTANT]
> The install script is now deprecated and will no longer receive updates.
> This guide is only for **existing** `install.sh` / `deploy/` deployments.
> Setting up SigNoz for the first time? Skip migration and install Foundry
> directly: [SigNoz install docs][install-docs].
This guide walks you through migrating an existing SigNoz deployment running via
Docker Compose to [Foundry](https://signoz.io/docs/install/docker/).
## How it works
> [!NOTE]
> Setting up SigNoz for the first time? You don't need this guide — follow the [SigNoz installation docs](https://signoz.io/docs/install/) instead.
Foundry splits a deployment into two commands:
## Overview
To stay up to date on new installation platforms and patterns, please refer to [Foundry](https://github.com/SigNoz/foundry).
- `foundryctl forge` generates the deployment manifests from a `casting.yaml`.
It never touches running containers, so it is safe to re-run while you
iterate.
- `foundryctl cast` applies those manifests: it (re)creates the containers and
reuses the volumes you point it at.
Two `foundryctl` commands are used throughout this guide:
- **`forge`** — generates deployment manifests from your `casting.yaml`. It does not touch running containers, so it is safe to re-run while you iterate.
- **`cast`** — applies the generated manifests: it creates and starts the containers (and pulls new images).
You write one `casting.yaml`, point a few patches at your existing data
volumes, then cast. The steps below are the same for Compose and Swarm; they
differ only in the casting (step 3) and how you stop the old stack (step 5).
## Prerequisites
- [ ] Install Foundry - `curl -fsSL https://signoz.io/foundry.sh | bash`
## Migration Steps
> [!WARNING]
> **Before proceeding, back up both:**
> - **Your docker volumes** — these hold your data.
> - **Your existing `docker-compose.yaml` (and any config it references)** — keep a copy somewhere safe. The compose manifests are no longer distributed by SigNoz, so this backup is your only way to roll back to your previous setup.
- An existing SigNoz deployment from `install.sh` or `deploy/` (Compose or
Swarm).
- `foundryctl` (installed in step 1).
1. Make a note of the volume names used by your existing deployment for the following components:
- ClickHouse
- SigNoz
- ZooKeeper
## Migrate
> If you used the docker compose file we provided, the volumes will be `signoz-clickhouse`, `signoz-sqlite`, and `signoz-zookeeper-1`.
### 1. Install Foundry
2. Generate your `casting.yaml`. Based on internal testing, the following casting should generate the manifests that mimic the legacy docker compose setup (compare against your backed-up `docker-compose.yaml`). Once created, run `foundryctl forge -f casting.yaml`.
```bash
curl -fsSL https://signoz.io/foundry.sh | bash
```
Foundry has a [Docker Compose example](https://github.com/SigNoz/foundry/tree/main/docs/examples/docker/compose). Please use it as a reference.
### 2. Keep your rollback path
> [!WARNING]
> If your deployment had more than 1 shard or replica, you will need to adjust your manifest volumes accordingly.
This migration reattaches your existing volumes in place; it does not move or
delete your data. The only destructive action is passing `--volumes` / `-v`
when you stop the old stack (step 5), so avoid that flag.
> [!IMPORTANT]
> The `replica` and `shard` macros below are placeholders. Replace them with the values from your existing ClickHouse configuration (check the `macros` section of your current ClickHouse config, e.g. `config.xml`/`metrika.xml`), otherwise the generated manifests will not match your existing data.
> Keep a copy of your existing `docker-compose.yaml` / stack file (and any
> config it references). SigNoz no longer distributes these files, so this copy
> is your only way to roll back.
### 3. Write your `casting.yaml`
Use the casting for your deployment. Both reproduce the legacy single-node
setup (ClickHouse + ZooKeeper + SQLite) and reattach your existing volumes;
they differ only in `spec.deployment.flavor` and the volume-reuse patch
(Compose volumes have a `name` to replace; Swarm volumes are bare, so the whole
entry is replaced). If your deployment ran more than one shard or replica,
adjust the volume patches accordingly. The
[Docker Compose example][compose-example] is a useful reference.
> [!IMPORTANT]
> The `replica` and `shard` macros are placeholders. Replace them with the
> values from your existing ClickHouse config (the `macros` section of
> `config.xml` / `metrika.xml`), or the generated manifests will not match your
> existing data.
<details>
<summary><b>Docker Compose</b> casting.yaml</summary>
```yaml
# casting.yaml
apiVersion: v1alpha1
kind: Installation
metadata:
@@ -61,8 +89,8 @@ spec:
data:
config-0-0.yaml: |
macros:
replica: "example01-01-1" # replace with your existing ClickHouse replica macro (see legacy configuration files for reference)
shard: "01" # replace with your existing ClickHouse shard macro (see legacy configuration files for reference)
replica: "example01-01-1" # replace with your replica macro
shard: "01" # replace with your shard macro
patches:
- target: "deployment/compose.yaml"
operations:
@@ -80,50 +108,165 @@ spec:
value: root
```
> [!NOTE]
> The `user: root` patch on the ZooKeeper service is required so the container can read/write the data in your reused ZooKeeper volume, which was created with `root`-owned files by the legacy compose setup. Without it, ZooKeeper may fail to start with permission errors.
</details>
If you had custom configurations for features like SMTP or additional ingestion processors/receivers, you will need to include those in your casting file via [patches](https://github.com/SigNoz/foundry/blob/main/docs/concepts/patches.md), [custom configuration](https://github.com/SigNoz/foundry/blob/main/docs/concepts/moldings.md#custom-config-files) or [environment variables](https://github.com/SigNoz/foundry/blob/main/docs/reference/casting-file.md#molding-spec) based on your previous configuration.
<details>
<summary><b>Docker Swarm</b> casting.yaml</summary>
3. Review your manifests, we suggest executing the following checks on your manifests before proceeding:
- [ ] Validate the container images match what your deployment had, Foundry uses `latest` on generation by default.
- [ ] If your signoz version was older than latest, please check the [upgrade path](https://signoz.io/docs/operate/upgrade/) first.
- [ ] Check the produced manifests in `pours/deployment` match your older configurations. Extra consideration and review needs to be done on `compose.yaml` as this will be the main entry point for your new deployment.
- [ ] The configuration files for clickhouse are now in YAML so validate your custom settings are present.
```yaml
# casting.yaml
apiVersion: v1alpha1
kind: Installation
metadata:
name: signoz
spec:
deployment:
flavor: swarm
mode: docker
metastore:
kind: sqlite
telemetrykeeper:
kind: zookeeper
telemetrystore:
spec:
config:
data:
config-0-0.yaml: |
macros:
replica: "example01-01-1" # replace with your replica macro
shard: "01" # replace with your shard macro
patches:
- target: "deployment/compose.yaml"
operations:
- op: replace
path: /volumes/signoz-telemetrykeeper-0-data
value:
name: signoz-zookeeper-1
- op: replace
path: /volumes/signoz-telemetrystore-0-0-data
value:
name: signoz-clickhouse
- op: replace
path: /volumes/signoz-metastore-sqlite-0-data
value:
name: signoz-sqlite
- op: add
path: /services/signoz-telemetrykeeper-zookeeper-0/user
value: root
```
4. Execute a `docker compose down`. **Do not** include parameters such as `--volumes` (or `-v`), as it will wipe the volumes we need to maintain and reuse to avoid data loss.
</details>
> [!NOTE]
> This will generate downtime so please plan accordingly.
> The `user: root` patch on the ZooKeeper service lets the container read and
> write the data in your reused ZooKeeper volume, whose files the legacy setup
> created as `root`. Without it, ZooKeeper may fail to start with permission
> errors.
5. Validate the SigNoz containers are down with `docker ps -a`. Multiple containers cannot bind the same volume.
If you had custom configuration (SMTP, extra ingestion receivers/processors,
or custom ClickHouse settings), carry it over via [patches][patches],
[custom config files][custom-config], or [environment variables][env-vars].
6. Run `foundryctl cast -f casting.yaml`. This will recreate the containers based on the spec. This process will download new container images.
### 4. Generate and review the manifests
```bash
foundryctl forge -f casting.yaml
```
Review `pours/deployment/` before deploying:
- [ ] Container images match your current deployment. Foundry generates with
`latest` by default; if your SigNoz version was older than latest, check the
[upgrade path][upgrade-path] first.
- [ ] The generated manifests match your previous configuration, especially
`compose.yaml` (the new entry point for your deployment).
- [ ] The ClickHouse config is now YAML rather than XML; confirm your custom
settings carried over (see [ClickHouse configuration files][ch-config] for
the XML-to-YAML mapping).
### 5. Stop the old deployment
Use the command for your deployment. Do **not** pass `--volumes` / `-v`; that
would delete the data you are migrating.
```bash
docker compose down # Compose
docker stack rm signoz # Swarm
```
> [!NOTE]
> When `cast` is run, the migration container will execute its migrations.
> This causes downtime, so plan accordingly.
## Verifying the Migration
- SigNoz containers will be up and running.
- Log in to the SigNoz UI and verify that data is present.
- Signoz will run on localhost:8080
- Validate that your data ingestion is receiving data.
- Ingesters will receive data on localhost:4317(grpc) and localhost:4318(http)
- Review the logs from both ClickHouse and ZooKeeper; no errors should be present.
Confirm nothing is still bound to the volumes before continuing:
## Rolling Back
Because step 4 brought the legacy stack down *without* `-v`, your original volumes
are untouched and still hold your data. To roll back:
```bash
docker ps -a
```
- Stop and remove the containers created by Foundry (`docker compose down`, again without `-v`).
- Confirm the containers are gone with `docker ps -a` so nothing else is bound to the volumes.
- Reapply your original docker compose file (`docker compose up -d`). It will reattach to the
existing volumes and restore your prior state.
### 6. Deploy with Foundry
```bash
foundryctl cast -f casting.yaml
```
This recreates the containers against your existing volumes and pulls the
images. The migration container runs the schema migrations as part of `cast`.
**Prefer not to use `cast`?** The manifests in `pours/deployment/` are standard
Docker artifacts you can apply yourself. Run the command from that directory so
the relative config paths resolve:
```bash
cd pours/deployment
docker compose up -d # Compose
docker stack deploy -c compose.yaml signoz # Swarm
```
## Verify
- All SigNoz containers are running.
- The UI is reachable on `http://localhost:8080`, and OTLP on `4317` (gRPC)
and `4318` (HTTP), so already-instrumented apps and saved bookmarks keep
working.
- Your existing data is present in the UI, and new data is being ingested.
- ClickHouse and ZooKeeper logs show no errors.
## Roll back
Step 5 left your volumes untouched, so your data is intact. To return to the
previous setup:
1. Bring down the Foundry deployment (`docker compose down` or
`docker stack rm signoz`, again without `-v`).
2. Confirm the containers are gone with `docker ps -a`.
3. Re-apply your backed-up stack: `docker compose up -d` (Compose) or
`docker stack deploy -c docker-compose.yaml signoz` (Swarm). It reattaches
the same volumes and restores your prior state.
## Troubleshooting
- Please reach out to our community on [Slack](https://signoz.io/slack).
If the migration runs into trouble, see
[Troubleshooting Foundry][troubleshooting] for how to capture what we need to
help (the `--debug` output, the exit code, and your `casting.yaml`), then reach
out on [Slack][slack].
## References
- [SigNoz Docker installation docs](https://signoz.io/docs/install/docker/)
- [SigNoz documentation](https://signoz.io/docs)
- [Foundry](https://github.com/SigNoz/foundry)
- [Foundry][foundry]
- [Casting file reference][casting-ref]
- [Custom config files][custom-config]
- [Patches][patches]
- [SigNoz documentation][signoz-docs]
[foundry]: https://github.com/SigNoz/foundry
[install-docs]: https://signoz.io/docs/install/
[compose-example]: https://github.com/SigNoz/foundry/tree/main/docs/examples/docker/compose
[patches]: https://github.com/SigNoz/foundry/blob/main/docs/concepts/patches.md
[custom-config]: https://github.com/SigNoz/foundry/blob/main/docs/concepts/moldings.md#custom-config-files
[env-vars]: https://github.com/SigNoz/foundry/blob/main/docs/reference/casting-file.md#molding-spec
[casting-ref]: https://github.com/SigNoz/foundry/blob/main/docs/reference/casting-file.md
[ch-config]: https://clickhouse.com/docs/operations/configuration-files
[upgrade-path]: https://signoz.io/docs/operate/upgrade/
[troubleshooting]: https://signoz.io/docs/setup/foundry/troubleshooting/faq/
[slack]: https://signoz.io/slack
[signoz-docs]: https://signoz.io/docs

View File

@@ -651,8 +651,6 @@ components:
$ref: '#/components/schemas/AuthtypesTransactionGroups'
required:
- name
- description
- transactionGroups
type: object
AuthtypesPostableRotateToken:
properties:
@@ -2407,6 +2405,46 @@ components:
to_user:
type: string
type: object
CoretypesKind:
enum:
- anonymous
- organization
- role
- serviceaccount
- user
- notification-channel
- route-policy
- apdex-setting
- auth-domain
- session
- cloud-integration
- cloud-integration-service
- integration
- dashboard
- public-dashboard
- ingestion-key
- ingestion-limit
- pipeline
- user-preference
- org-preference
- quick-filter
- ttl-setting
- rule
- planned-maintenance
- saved-view
- trace-funnel
- factor-password
- factor-api-key
- license
- subscription
- logs
- traces
- metrics
- audit-logs
- meter-metrics
- logs-field
- traces-field
type: string
CoretypesObject:
properties:
resource:
@@ -2448,7 +2486,7 @@ components:
CoretypesResourceRef:
properties:
kind:
type: string
$ref: '#/components/schemas/CoretypesKind'
type:
$ref: '#/components/schemas/CoretypesType'
required:
@@ -4016,6 +4054,8 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesClusterRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4026,6 +4066,7 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesDaemonSetRecord:
@@ -4082,6 +4123,8 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesDaemonSetRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4092,6 +4135,7 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesDeploymentRecord:
@@ -4148,6 +4192,8 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesDeploymentRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4158,6 +4204,7 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesHostFilter:
@@ -4223,6 +4270,8 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesHostRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4233,6 +4282,7 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesJobRecord:
@@ -4295,6 +4345,8 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesJobRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4305,6 +4357,7 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesNamespaceRecord:
@@ -4339,6 +4392,8 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesNamespaceRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4349,6 +4404,7 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesNodeCondition:
@@ -4413,6 +4469,8 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesNodeRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4423,6 +4481,7 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesPodCountsByPhase:
@@ -4508,6 +4567,8 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesPodRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4518,6 +4579,7 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesPostableClusters:
@@ -4780,6 +4842,16 @@ components:
- end
- limit
type: object
InframonitoringtypesRequiredMetricsCheck:
properties:
missingMetrics:
items:
type: string
nullable: true
type: array
required:
- missingMetrics
type: object
InframonitoringtypesResponseType:
enum:
- list
@@ -4839,6 +4911,8 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesStatefulSetRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4849,6 +4923,7 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesVolumeRecord:
@@ -4896,6 +4971,8 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesVolumeRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4906,6 +4983,7 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
LlmpricingruletypesGettablePricingRules:
@@ -14804,10 +14882,10 @@ paths:
for custom groupBy keys; in both modes every row aggregates nodes and pods
in the group. Supports filtering via a filter expression, custom groupBy,
ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination
via offset/limit. Also reports whether the requested time range falls before
the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable,
clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data
is available for that field.'
via offset/limit. Also reports missing required metrics and whether the requested
time range falls before the data retention boundary. Numeric metric fields
(clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable)
return -1 as a sentinel when no data is available for that field.'
operationId: ListClusters
requestBody:
content:
@@ -14880,11 +14958,11 @@ paths:
row aggregates pods owned by daemonsets in the group. Supports filtering via
a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit
/ memory / memory_request / memory_limit / desired_nodes / current_nodes,
and pagination via offset/limit. Also reports whether the requested time range
falls before the data retention boundary. Numeric metric fields (daemonSetCPU,
daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest,
daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel
when no data is available for that field.'
and pagination via offset/limit. Also reports missing required metrics and
whether the requested time range falls before the data retention boundary.
Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit,
daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes,
currentNodes) return -1 as a sentinel when no data is available for that field.'
operationId: ListDaemonSets
requestBody:
content:
@@ -14955,11 +15033,11 @@ paths:
group. Supports filtering via a filter expression, custom groupBy, ordering
by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit
/ desired_pods / available_pods, and pagination via offset/limit. Also reports
whether the requested time range falls before the data retention boundary.
Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit,
deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods,
availablePods) return -1 as a sentinel when no data is available for that
field.'
missing required metrics and whether the requested time range falls before
the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest,
deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit,
desiredPods, availablePods) return -1 as a sentinel when no data is available
for that field.'
operationId: ListDeployments
requestBody:
content:
@@ -15024,9 +15102,10 @@ paths:
custom groupBy to aggregate hosts by any attribute, ordering by any of the
five metrics, and pagination via offset/limit. The response type is ''list''
for the default host.name grouping or ''grouped_list'' for custom groupBy
keys. Also reports whether the requested time range falls before the data
retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage)
return -1 as a sentinel when no data is available for that field.'
keys. Also reports missing required metrics and whether the requested time
range falls before the data retention boundary. Numeric metric fields (cpu,
memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available
for that field.'
operationId: ListHosts
requestBody:
content:
@@ -15100,11 +15179,11 @@ paths:
jobs in the group. Supports filtering via a filter expression, custom groupBy,
ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit
/ desired_successful_pods / active_pods / failed_pods / successful_pods, and
pagination via offset/limit. Also reports whether the requested time range
falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest,
jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods,
activePods, failedPods, successfulPods) return -1 as a sentinel when no data
is available for that field.'
pagination via offset/limit. Also reports missing required metrics and whether
the requested time range falls before the data retention boundary. Numeric
metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest,
jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods)
return -1 as a sentinel when no data is available for that field.'
operationId: ListJobs
requestBody:
content:
@@ -15169,10 +15248,10 @@ paths:
type is ''list'' for the default k8s.namespace.name grouping or ''grouped_list''
for custom groupBy keys; in both modes every row aggregates pods in the group.
Supports filtering via a filter expression, custom groupBy, ordering by cpu
/ memory, and pagination via offset/limit. Also reports whether the requested
time range falls before the data retention boundary. Numeric metric fields
(namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available
for that field.'
/ memory, and pagination via offset/limit. Also reports missing required metrics
and whether the requested time range falls before the data retention boundary.
Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel
when no data is available for that field.'
operationId: ListNamespaces
requestBody:
content:
@@ -15240,10 +15319,10 @@ paths:
for custom groupBy keys (each row aggregates nodes in the group; condition
stays no_data). Supports filtering via a filter expression, custom groupBy,
ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination
via offset/limit. Also reports whether the requested time range falls before
the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable,
nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is
available for that field.'
via offset/limit. Also reports missing required metrics and whether the requested
time range falls before the data retention boundary. Numeric metric fields
(nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1
as a sentinel when no data is available for that field.'
operationId: ListNodes
requestBody:
content:
@@ -15312,10 +15391,11 @@ paths:
is one pod with its current phase) or ''grouped_list'' for custom groupBy
keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase:
{ pending, running, succeeded, failed, unknown } derived from each pod''s
latest phase in the window). Also reports whether the requested time range
falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest,
podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1
as a sentinel when no data is available for that field.'
latest phase in the window). Also reports missing required metrics and whether
the requested time range falls before the data retention boundary. Numeric
metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest,
podMemoryLimit, podAge) return -1 as a sentinel when no data is available
for that field.'
operationId: ListPods
requestBody:
content:
@@ -15382,10 +15462,11 @@ paths:
usage, inodes, inodes_free, inodes_used), and pagination via offset/limit.
The response type is ''list'' for the default k8s.persistentvolumeclaim.name
grouping or ''grouped_list'' for custom groupBy keys; in both modes every
row aggregates volumes in the group. Also reports whether the requested time
range falls before the data retention boundary. Numeric metric fields (volumeAvailable,
volumeCapacity, volumeUsage, volumeInodes, volumeInodesFree, volumeInodesUsed)
return -1 as a sentinel when no data is available for that field.'
row aggregates volumes in the group. Also reports missing required metrics
and whether the requested time range falls before the data retention boundary.
Numeric metric fields (volumeAvailable, volumeCapacity, volumeUsage, volumeInodes,
volumeInodesFree, volumeInodesUsed) return -1 as a sentinel when no data is
available for that field.'
operationId: ListVolumes
requestBody:
content:
@@ -15456,10 +15537,11 @@ paths:
statefulsets in the group. Supports filtering via a filter expression, custom
groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request
/ memory_limit / desired_pods / current_pods, and pagination via offset/limit.
Also reports whether the requested time range falls before the data retention
boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit,
statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods,
currentPods) return -1 as a sentinel when no data is available for that field.'
Also reports missing required metrics and whether the requested time range
falls before the data retention boundary. Numeric metric fields (statefulSetCPU,
statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest,
statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel
when no data is available for that field.'
operationId: ListStatefulSets
requestBody:
content:
@@ -15618,16 +15700,20 @@ paths:
summary: List metric names
tags:
- metrics
/api/v2/metrics/{metric_name}/alerts:
/api/v2/metrics/alerts:
get:
deprecated: false
description: This endpoint returns associated alerts for a specified metric
operationId: GetMetricAlerts
parameters:
- in: path
name: metric_name
- description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
in: query
name: metricName
required: true
schema:
description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
type: string
responses:
"200":
@@ -15682,28 +15768,36 @@ paths:
summary: Get metric alerts
tags:
- metrics
/api/v2/metrics/{metric_name}/attributes:
/api/v2/metrics/attributes:
get:
deprecated: false
description: This endpoint returns attribute keys and their unique values for
a specified metric
operationId: GetMetricAttributes
parameters:
- in: query
name: start
schema:
nullable: true
type: integer
- in: query
name: end
schema:
nullable: true
type: integer
- in: path
name: metric_name
- description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
in: query
name: metricName
required: true
schema:
description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
type: string
- description: Start of the time range as a Unix timestamp in milliseconds.
in: query
name: start
schema:
description: Start of the time range as a Unix timestamp in milliseconds.
nullable: true
type: integer
- description: End of the time range as a Unix timestamp in milliseconds.
in: query
name: end
schema:
description: End of the time range as a Unix timestamp in milliseconds.
nullable: true
type: integer
responses:
"200":
content:
@@ -15757,16 +15851,20 @@ paths:
summary: Get metric attributes
tags:
- metrics
/api/v2/metrics/{metric_name}/dashboards:
/api/v2/metrics/dashboards:
get:
deprecated: false
description: This endpoint returns associated dashboards for a specified metric
operationId: GetMetricDashboards
parameters:
- in: path
name: metric_name
- description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
in: query
name: metricName
required: true
schema:
description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
type: string
responses:
"200":
@@ -15821,17 +15919,21 @@ paths:
summary: Get metric dashboards
tags:
- metrics
/api/v2/metrics/{metric_name}/highlights:
/api/v2/metrics/highlights:
get:
deprecated: false
description: This endpoint returns highlights like number of datapoints, totaltimeseries,
active time series, last received time for a specified metric
operationId: GetMetricHighlights
parameters:
- in: path
name: metric_name
- description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
in: query
name: metricName
required: true
schema:
description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
type: string
responses:
"200":
@@ -15886,17 +15988,79 @@ paths:
summary: Get metric highlights
tags:
- metrics
/api/v2/metrics/{metric_name}/metadata:
/api/v2/metrics/inspect:
post:
deprecated: false
description: Returns raw time series data points for a metric within a time
range (max 30 minutes). Each series includes labels and timestamp/value pairs.
operationId: InspectMetrics
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/MetricsexplorertypesInspectMetricsRequest'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricsexplorertypesInspectMetricsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Inspect raw metric data points
tags:
- metrics
/api/v2/metrics/metadata:
get:
deprecated: false
description: This endpoint returns metadata information like metric description,
unit, type, temporality, monotonicity for a specified metric
operationId: GetMetricMetadata
parameters:
- in: path
name: metric_name
- description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
in: query
name: metricName
required: true
schema:
description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
type: string
responses:
"200":
@@ -15956,12 +16120,6 @@ paths:
description: This endpoint helps to update metadata information like metric
description, unit, type, temporality, monotonicity for a specified metric
operationId: UpdateMetricMetadata
parameters:
- in: path
name: metric_name
required: true
schema:
type: string
requestBody:
content:
application/json:
@@ -16002,64 +16160,6 @@ paths:
summary: Update metric metadata
tags:
- metrics
/api/v2/metrics/inspect:
post:
deprecated: false
description: Returns raw time series data points for a metric within a time
range (max 30 minutes). Each series includes labels and timestamp/value pairs.
operationId: InspectMetrics
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/MetricsexplorertypesInspectMetricsRequest'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricsexplorertypesInspectMetricsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Inspect raw metric data points
tags:
- metrics
/api/v2/metrics/onboarding:
get:
deprecated: false

View File

@@ -25,7 +25,7 @@
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:config:web-settings": "json2ts ../docs/config/web-settings.json -o src/types/generated/webSettings.ts --style.useTabs --style.tabWidth=1 --style.singleQuote --bannerComment '/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM docs/config/web-settings.json */'"
"generate:config:web-settings": "json2ts ./src/schemas/generated/webSettings.schema.json -o src/types/generated/webSettings.ts --style.useTabs --style.tabWidth=1 --style.singleQuote --bannerComment '/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM frontend/src/schemas/generated/webSettings.schema.json */'"
},
"engines": {
"node": ">=22.0.0",

View File

@@ -39,7 +39,7 @@ import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* @summary List Clusters for Infra Monitoring
*/
export const listClusters = (
@@ -122,7 +122,7 @@ export const useListClusters = <
return useMutation(getListClustersMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.
* @summary List DaemonSets for Infra Monitoring
*/
export const listDaemonSets = (
@@ -205,7 +205,7 @@ export const useListDaemonSets = <
return useMutation(getListDaemonSetsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.
* @summary List Deployments for Infra Monitoring
*/
export const listDeployments = (
@@ -288,7 +288,7 @@ export const useListDeployments = <
return useMutation(getListDeploymentsMutationOptions(options));
};
/**
* Returns a paginated list of hosts with key infrastructure metrics: CPU usage (%), memory usage (%), I/O wait (%), disk usage (%), and 15-minute load average. Each host includes its current status (active/inactive based on metrics reported in the last 10 minutes) and metadata attributes (e.g., os.type). Supports filtering via a filter expression, filtering by host status, custom groupBy to aggregate hosts by any attribute, ordering by any of the five metrics, and pagination via offset/limit. The response type is 'list' for the default host.name grouping or 'grouped_list' for custom groupBy keys. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of hosts with key infrastructure metrics: CPU usage (%), memory usage (%), I/O wait (%), disk usage (%), and 15-minute load average. Each host includes its current status (active/inactive based on metrics reported in the last 10 minutes) and metadata attributes (e.g., os.type). Supports filtering via a filter expression, filtering by host status, custom groupBy to aggregate hosts by any attribute, ordering by any of the five metrics, and pagination via offset/limit. The response type is 'list' for the default host.name grouping or 'grouped_list' for custom groupBy keys. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available for that field.
* @summary List Hosts for Infra Monitoring
*/
export const listHosts = (
@@ -371,7 +371,7 @@ export const useListHosts = <
return useMutation(getListHostsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.
* @summary List Jobs for Infra Monitoring
*/
export const listJobs = (
@@ -454,7 +454,7 @@ export const useListJobs = <
return useMutation(getListJobsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
* @summary List Namespaces for Infra Monitoring
*/
export const listNamespaces = (
@@ -537,7 +537,7 @@ export const useListNamespaces = <
return useMutation(getListNamespacesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* @summary List Nodes for Infra Monitoring
*/
export const listNodes = (
@@ -620,7 +620,7 @@ export const useListNodes = <
return useMutation(getListNodesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase: { pending, running, succeeded, failed, unknown } derived from each pod's latest phase in the window). Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase: { pending, running, succeeded, failed, unknown } derived from each pod's latest phase in the window). Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.
* @summary List Pods for Infra Monitoring
*/
export const listPods = (
@@ -703,7 +703,7 @@ export const useListPods = <
return useMutation(getListPodsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes persistent volume claims (PVCs) with key volume metrics: available bytes, capacity bytes, usage (capacity - available), inodes, free inodes, and used inodes. Each row also includes metadata attributes (k8s.persistentvolumeclaim.name, k8s.pod.uid, k8s.pod.name, k8s.namespace.name, k8s.node.name, k8s.statefulset.name, k8s.cluster.name). Supports filtering via a filter expression, custom groupBy to aggregate volumes by any attribute, ordering by any of the six metrics (available, capacity, usage, inodes, inodes_free, inodes_used), and pagination via offset/limit. The response type is 'list' for the default k8s.persistentvolumeclaim.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates volumes in the group. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (volumeAvailable, volumeCapacity, volumeUsage, volumeInodes, volumeInodesFree, volumeInodesUsed) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes persistent volume claims (PVCs) with key volume metrics: available bytes, capacity bytes, usage (capacity - available), inodes, free inodes, and used inodes. Each row also includes metadata attributes (k8s.persistentvolumeclaim.name, k8s.pod.uid, k8s.pod.name, k8s.namespace.name, k8s.node.name, k8s.statefulset.name, k8s.cluster.name). Supports filtering via a filter expression, custom groupBy to aggregate volumes by any attribute, ordering by any of the six metrics (available, capacity, usage, inodes, inodes_free, inodes_used), and pagination via offset/limit. The response type is 'list' for the default k8s.persistentvolumeclaim.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates volumes in the group. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (volumeAvailable, volumeCapacity, volumeUsage, volumeInodes, volumeInodesFree, volumeInodesUsed) return -1 as a sentinel when no data is available for that field.
* @summary List Volumes for Infra Monitoring
*/
export const listVolumes = (
@@ -786,7 +786,7 @@ export const useListVolumes = <
return useMutation(getListVolumesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.
* @summary List StatefulSets for Infra Monitoring
*/
export const listStatefulSets = (

View File

@@ -19,16 +19,15 @@ import type {
import type {
GetMetricAlerts200,
GetMetricAlertsPathParameters,
GetMetricAlertsParams,
GetMetricAttributes200,
GetMetricAttributesParams,
GetMetricAttributesPathParameters,
GetMetricDashboards200,
GetMetricDashboardsPathParameters,
GetMetricDashboardsParams,
GetMetricHighlights200,
GetMetricHighlightsPathParameters,
GetMetricHighlightsParams,
GetMetricMetadata200,
GetMetricMetadataPathParameters,
GetMetricMetadataParams,
GetMetricsOnboardingStatus200,
GetMetricsStats200,
GetMetricsTreemap200,
@@ -40,7 +39,6 @@ import type {
MetricsexplorertypesTreemapRequestDTO,
MetricsexplorertypesUpdateMetricMetadataRequestDTO,
RenderErrorResponseDTO,
UpdateMetricMetadataPathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
@@ -146,27 +144,26 @@ export const invalidateListMetrics = async (
* @summary Get metric alerts
*/
export const getMetricAlerts = (
{ metricName }: GetMetricAlertsPathParameters,
params: GetMetricAlertsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricAlerts200>({
url: `/api/v2/metrics/${metricName}/alerts`,
url: `/api/v2/metrics/alerts`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricAlertsQueryKey = ({
metricName,
}: GetMetricAlertsPathParameters) => {
return [`/api/v2/metrics/${metricName}/alerts`] as const;
export const getGetMetricAlertsQueryKey = (params?: GetMetricAlertsParams) => {
return [`/api/v2/metrics/alerts`, ...(params ? [params] : [])] as const;
};
export const getGetMetricAlertsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricAlerts>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricAlertsPathParameters,
params: GetMetricAlertsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAlerts>>,
@@ -177,19 +174,13 @@ export const getGetMetricAlertsQueryOptions = <
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricAlertsQueryKey({ metricName });
const queryKey = queryOptions?.queryKey ?? getGetMetricAlertsQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getMetricAlerts>>> = ({
signal,
}) => getMetricAlerts({ metricName }, signal);
}) => getMetricAlerts(params, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricAlerts>>,
TError,
TData
@@ -209,7 +200,7 @@ export function useGetMetricAlerts<
TData = Awaited<ReturnType<typeof getMetricAlerts>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricAlertsPathParameters,
params: GetMetricAlertsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAlerts>>,
@@ -218,7 +209,7 @@ export function useGetMetricAlerts<
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricAlertsQueryOptions({ metricName }, options);
const queryOptions = getGetMetricAlertsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -232,11 +223,11 @@ export function useGetMetricAlerts<
*/
export const invalidateGetMetricAlerts = async (
queryClient: QueryClient,
{ metricName }: GetMetricAlertsPathParameters,
params: GetMetricAlertsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricAlertsQueryKey({ metricName }) },
{ queryKey: getGetMetricAlertsQueryKey(params) },
options,
);
@@ -248,12 +239,11 @@ export const invalidateGetMetricAlerts = async (
* @summary Get metric attributes
*/
export const getMetricAttributes = (
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
params: GetMetricAttributesParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricAttributes200>({
url: `/api/v2/metrics/${metricName}/attributes`,
url: `/api/v2/metrics/attributes`,
method: 'GET',
params,
signal,
@@ -261,21 +251,16 @@ export const getMetricAttributes = (
};
export const getGetMetricAttributesQueryKey = (
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
) => {
return [
`/api/v2/metrics/${metricName}/attributes`,
...(params ? [params] : []),
] as const;
return [`/api/v2/metrics/attributes`, ...(params ? [params] : [])] as const;
};
export const getGetMetricAttributesQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricAttributes>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
params: GetMetricAttributesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAttributes>>,
@@ -287,19 +272,13 @@ export const getGetMetricAttributesQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetMetricAttributesQueryKey({ metricName }, params);
queryOptions?.queryKey ?? getGetMetricAttributesQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricAttributes>>
> = ({ signal }) => getMetricAttributes({ metricName }, params, signal);
> = ({ signal }) => getMetricAttributes(params, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricAttributes>>,
TError,
TData
@@ -319,8 +298,7 @@ export function useGetMetricAttributes<
TData = Awaited<ReturnType<typeof getMetricAttributes>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
params: GetMetricAttributesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAttributes>>,
@@ -329,11 +307,7 @@ export function useGetMetricAttributes<
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricAttributesQueryOptions(
{ metricName },
params,
options,
);
const queryOptions = getGetMetricAttributesQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -347,12 +321,11 @@ export function useGetMetricAttributes<
*/
export const invalidateGetMetricAttributes = async (
queryClient: QueryClient,
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
params: GetMetricAttributesParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricAttributesQueryKey({ metricName }, params) },
{ queryKey: getGetMetricAttributesQueryKey(params) },
options,
);
@@ -364,27 +337,28 @@ export const invalidateGetMetricAttributes = async (
* @summary Get metric dashboards
*/
export const getMetricDashboards = (
{ metricName }: GetMetricDashboardsPathParameters,
params: GetMetricDashboardsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricDashboards200>({
url: `/api/v2/metrics/${metricName}/dashboards`,
url: `/api/v2/metrics/dashboards`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricDashboardsQueryKey = ({
metricName,
}: GetMetricDashboardsPathParameters) => {
return [`/api/v2/metrics/${metricName}/dashboards`] as const;
export const getGetMetricDashboardsQueryKey = (
params?: GetMetricDashboardsParams,
) => {
return [`/api/v2/metrics/dashboards`, ...(params ? [params] : [])] as const;
};
export const getGetMetricDashboardsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricDashboardsPathParameters,
params: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
@@ -396,18 +370,13 @@ export const getGetMetricDashboardsQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricDashboardsQueryKey({ metricName });
queryOptions?.queryKey ?? getGetMetricDashboardsQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricDashboards>>
> = ({ signal }) => getMetricDashboards({ metricName }, signal);
> = ({ signal }) => getMetricDashboards(params, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
TError,
TData
@@ -427,7 +396,7 @@ export function useGetMetricDashboards<
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricDashboardsPathParameters,
params: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
@@ -436,10 +405,7 @@ export function useGetMetricDashboards<
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricDashboardsQueryOptions(
{ metricName },
options,
);
const queryOptions = getGetMetricDashboardsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -453,11 +419,11 @@ export function useGetMetricDashboards<
*/
export const invalidateGetMetricDashboards = async (
queryClient: QueryClient,
{ metricName }: GetMetricDashboardsPathParameters,
params: GetMetricDashboardsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricDashboardsQueryKey({ metricName }) },
{ queryKey: getGetMetricDashboardsQueryKey(params) },
options,
);
@@ -469,27 +435,28 @@ export const invalidateGetMetricDashboards = async (
* @summary Get metric highlights
*/
export const getMetricHighlights = (
{ metricName }: GetMetricHighlightsPathParameters,
params: GetMetricHighlightsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricHighlights200>({
url: `/api/v2/metrics/${metricName}/highlights`,
url: `/api/v2/metrics/highlights`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricHighlightsQueryKey = ({
metricName,
}: GetMetricHighlightsPathParameters) => {
return [`/api/v2/metrics/${metricName}/highlights`] as const;
export const getGetMetricHighlightsQueryKey = (
params?: GetMetricHighlightsParams,
) => {
return [`/api/v2/metrics/highlights`, ...(params ? [params] : [])] as const;
};
export const getGetMetricHighlightsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricHighlights>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricHighlightsPathParameters,
params: GetMetricHighlightsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricHighlights>>,
@@ -501,18 +468,13 @@ export const getGetMetricHighlightsQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricHighlightsQueryKey({ metricName });
queryOptions?.queryKey ?? getGetMetricHighlightsQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricHighlights>>
> = ({ signal }) => getMetricHighlights({ metricName }, signal);
> = ({ signal }) => getMetricHighlights(params, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricHighlights>>,
TError,
TData
@@ -532,7 +494,7 @@ export function useGetMetricHighlights<
TData = Awaited<ReturnType<typeof getMetricHighlights>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricHighlightsPathParameters,
params: GetMetricHighlightsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricHighlights>>,
@@ -541,10 +503,7 @@ export function useGetMetricHighlights<
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricHighlightsQueryOptions(
{ metricName },
options,
);
const queryOptions = getGetMetricHighlightsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -558,219 +517,17 @@ export function useGetMetricHighlights<
*/
export const invalidateGetMetricHighlights = async (
queryClient: QueryClient,
{ metricName }: GetMetricHighlightsPathParameters,
params: GetMetricHighlightsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricHighlightsQueryKey({ metricName }) },
{ queryKey: getGetMetricHighlightsQueryKey(params) },
options,
);
return queryClient;
};
/**
* This endpoint returns metadata information like metric description, unit, type, temporality, monotonicity for a specified metric
* @summary Get metric metadata
*/
export const getMetricMetadata = (
{ metricName }: GetMetricMetadataPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricMetadata200>({
url: `/api/v2/metrics/${metricName}/metadata`,
method: 'GET',
signal,
});
};
export const getGetMetricMetadataQueryKey = ({
metricName,
}: GetMetricMetadataPathParameters) => {
return [`/api/v2/metrics/${metricName}/metadata`] as const;
};
export const getGetMetricMetadataQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricMetadataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricMetadataQueryKey({ metricName });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricMetadata>>
> = ({ signal }) => getMetricMetadata({ metricName }, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricMetadataQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricMetadata>>
>;
export type GetMetricMetadataQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get metric metadata
*/
export function useGetMetricMetadata<
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricMetadataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricMetadataQueryOptions({ metricName }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get metric metadata
*/
export const invalidateGetMetricMetadata = async (
queryClient: QueryClient,
{ metricName }: GetMetricMetadataPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricMetadataQueryKey({ metricName }) },
options,
);
return queryClient;
};
/**
* This endpoint helps to update metadata information like metric description, unit, type, temporality, monotonicity for a specified metric
* @summary Update metric metadata
*/
export const updateMetricMetadata = (
{ metricName }: UpdateMetricMetadataPathParameters,
metricsexplorertypesUpdateMetricMetadataRequestDTO?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/metrics/${metricName}/metadata`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: metricsexplorertypesUpdateMetricMetadataRequestDTO,
signal,
});
};
export const getUpdateMetricMetadataMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{
pathParams: UpdateMetricMetadataPathParameters;
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{
pathParams: UpdateMetricMetadataPathParameters;
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
},
TContext
> => {
const mutationKey = ['updateMetricMetadata'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateMetricMetadata>>,
{
pathParams: UpdateMetricMetadataPathParameters;
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateMetricMetadata(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateMetricMetadataMutationResult = NonNullable<
Awaited<ReturnType<typeof updateMetricMetadata>>
>;
export type UpdateMetricMetadataMutationBody =
| BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>
| undefined;
export type UpdateMetricMetadataMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update metric metadata
*/
export const useUpdateMetricMetadata = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{
pathParams: UpdateMetricMetadataPathParameters;
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{
pathParams: UpdateMetricMetadataPathParameters;
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
},
TContext
> => {
return useMutation(getUpdateMetricMetadataMutationOptions(options));
};
/**
* Returns raw time series data points for a metric within a time range (max 30 minutes). Each series includes labels and timestamp/value pairs.
* @summary Inspect raw metric data points
@@ -854,6 +611,188 @@ export const useInspectMetrics = <
> => {
return useMutation(getInspectMetricsMutationOptions(options));
};
/**
* This endpoint returns metadata information like metric description, unit, type, temporality, monotonicity for a specified metric
* @summary Get metric metadata
*/
export const getMetricMetadata = (
params: GetMetricMetadataParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricMetadata200>({
url: `/api/v2/metrics/metadata`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricMetadataQueryKey = (
params?: GetMetricMetadataParams,
) => {
return [`/api/v2/metrics/metadata`, ...(params ? [params] : [])] as const;
};
export const getGetMetricMetadataQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params: GetMetricMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricMetadataQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricMetadata>>
> = ({ signal }) => getMetricMetadata(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricMetadataQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricMetadata>>
>;
export type GetMetricMetadataQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get metric metadata
*/
export function useGetMetricMetadata<
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params: GetMetricMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricMetadataQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get metric metadata
*/
export const invalidateGetMetricMetadata = async (
queryClient: QueryClient,
params: GetMetricMetadataParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricMetadataQueryKey(params) },
options,
);
return queryClient;
};
/**
* This endpoint helps to update metadata information like metric description, unit, type, temporality, monotonicity for a specified metric
* @summary Update metric metadata
*/
export const updateMetricMetadata = (
metricsexplorertypesUpdateMetricMetadataRequestDTO?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/metrics/metadata`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: metricsexplorertypesUpdateMetricMetadataRequestDTO,
signal,
});
};
export const getUpdateMetricMetadataMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> },
TContext
> => {
const mutationKey = ['updateMetricMetadata'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateMetricMetadata>>,
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> }
> = (props) => {
const { data } = props ?? {};
return updateMetricMetadata(data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateMetricMetadataMutationResult = NonNullable<
Awaited<ReturnType<typeof updateMetricMetadata>>
>;
export type UpdateMetricMetadataMutationBody =
| BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>
| undefined;
export type UpdateMetricMetadataMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update metric metadata
*/
export const useUpdateMetricMetadata = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> },
TContext
> => {
return useMutation(getUpdateMetricMetadataMutationOptions(options));
};
/**
* Lightweight endpoint that checks if any non-SigNoz metrics have been ingested, used for onboarding status detection
* @summary Check if non-SigNoz metrics have been received

View File

@@ -2094,6 +2094,45 @@ export interface AuthtypesGettableTokenDTO {
tokenType?: string;
}
export enum CoretypesKindDTO {
anonymous = 'anonymous',
organization = 'organization',
role = 'role',
serviceaccount = 'serviceaccount',
user = 'user',
'notification-channel' = 'notification-channel',
'route-policy' = 'route-policy',
'apdex-setting' = 'apdex-setting',
'auth-domain' = 'auth-domain',
session = 'session',
'cloud-integration' = 'cloud-integration',
'cloud-integration-service' = 'cloud-integration-service',
integration = 'integration',
dashboard = 'dashboard',
'public-dashboard' = 'public-dashboard',
'ingestion-key' = 'ingestion-key',
'ingestion-limit' = 'ingestion-limit',
pipeline = 'pipeline',
'user-preference' = 'user-preference',
'org-preference' = 'org-preference',
'quick-filter' = 'quick-filter',
'ttl-setting' = 'ttl-setting',
rule = 'rule',
'planned-maintenance' = 'planned-maintenance',
'saved-view' = 'saved-view',
'trace-funnel' = 'trace-funnel',
'factor-password' = 'factor-password',
'factor-api-key' = 'factor-api-key',
license = 'license',
subscription = 'subscription',
logs = 'logs',
traces = 'traces',
metrics = 'metrics',
'audit-logs' = 'audit-logs',
'meter-metrics' = 'meter-metrics',
'logs-field' = 'logs-field',
'traces-field' = 'traces-field',
}
export enum CoretypesTypeDTO {
user = 'user',
serviceaccount = 'serviceaccount',
@@ -2104,10 +2143,7 @@ export enum CoretypesTypeDTO {
telemetryresource = 'telemetryresource',
}
export interface CoretypesResourceRefDTO {
/**
* @type string
*/
kind: string;
kind: CoretypesKindDTO;
type: CoretypesTypeDTO;
}
@@ -2243,12 +2279,12 @@ export interface AuthtypesPostableRoleDTO {
/**
* @type string
*/
description: string;
description?: string;
/**
* @type string
*/
name: string;
transactionGroups: AuthtypesTransactionGroupsDTO;
transactionGroups?: AuthtypesTransactionGroupsDTO;
}
export interface AuthtypesPostableRotateTokenDTO {
@@ -5499,6 +5535,13 @@ export interface InframonitoringtypesClusterRecordDTO {
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
}
export interface InframonitoringtypesRequiredMetricsCheckDTO {
/**
* @type array,null
*/
missingMetrics: string[] | null;
}
export enum InframonitoringtypesResponseTypeDTO {
list = 'list',
grouped_list = 'grouped_list',
@@ -5534,6 +5577,7 @@ export interface InframonitoringtypesClustersDTO {
* @type array
*/
records: InframonitoringtypesClusterRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5611,6 +5655,7 @@ export interface InframonitoringtypesDaemonSetsDTO {
* @type array
*/
records: InframonitoringtypesDaemonSetRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5688,6 +5733,7 @@ export interface InframonitoringtypesDeploymentsDTO {
* @type array
*/
records: InframonitoringtypesDeploymentRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5773,6 +5819,7 @@ export interface InframonitoringtypesHostsDTO {
* @type array
*/
records: InframonitoringtypesHostRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5858,6 +5905,7 @@ export interface InframonitoringtypesJobsDTO {
* @type array
*/
records: InframonitoringtypesJobRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5907,6 +5955,7 @@ export interface InframonitoringtypesNamespacesDTO {
* @type array
*/
records: InframonitoringtypesNamespaceRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5973,6 +6022,7 @@ export interface InframonitoringtypesNodesDTO {
* @type array
*/
records: InframonitoringtypesNodeRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -6056,6 +6106,7 @@ export interface InframonitoringtypesPodsDTO {
* @type array
*/
records: InframonitoringtypesPodRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -6403,6 +6454,7 @@ export interface InframonitoringtypesStatefulSetsDTO {
* @type array
*/
records: InframonitoringtypesStatefulSetRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -6471,6 +6523,7 @@ export interface InframonitoringtypesVolumesDTO {
* @type array
*/
records: InframonitoringtypesVolumeRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -10317,9 +10370,14 @@ export type ListMetrics200 = {
status: string;
};
export type GetMetricAlertsPathParameters = {
export type GetMetricAlertsParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
metricName: string;
};
export type GetMetricAlerts200 = {
data: MetricsexplorertypesMetricAlertsResponseDTO;
/**
@@ -10328,18 +10386,20 @@ export type GetMetricAlerts200 = {
status: string;
};
export type GetMetricAttributesPathParameters = {
metricName: string;
};
export type GetMetricAttributesParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
metricName: string;
/**
* @type integer,null
* @description undefined
* @description Start of the time range as a Unix timestamp in milliseconds.
*/
start?: number | null;
/**
* @type integer,null
* @description undefined
* @description End of the time range as a Unix timestamp in milliseconds.
*/
end?: number | null;
};
@@ -10352,9 +10412,14 @@ export type GetMetricAttributes200 = {
status: string;
};
export type GetMetricDashboardsPathParameters = {
export type GetMetricDashboardsParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
metricName: string;
};
export type GetMetricDashboards200 = {
data: MetricsexplorertypesMetricDashboardsResponseDTO;
/**
@@ -10363,9 +10428,14 @@ export type GetMetricDashboards200 = {
status: string;
};
export type GetMetricHighlightsPathParameters = {
export type GetMetricHighlightsParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
metricName: string;
};
export type GetMetricHighlights200 = {
data: MetricsexplorertypesMetricHighlightsResponseDTO;
/**
@@ -10374,22 +10444,24 @@ export type GetMetricHighlights200 = {
status: string;
};
export type GetMetricMetadataPathParameters = {
metricName: string;
};
export type GetMetricMetadata200 = {
data: MetricsexplorertypesMetricMetadataDTO;
export type InspectMetrics200 = {
data: MetricsexplorertypesInspectMetricsResponseDTO;
/**
* @type string
*/
status: string;
};
export type UpdateMetricMetadataPathParameters = {
export type GetMetricMetadataParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
metricName: string;
};
export type InspectMetrics200 = {
data: MetricsexplorertypesInspectMetricsResponseDTO;
export type GetMetricMetadata200 = {
data: MetricsexplorertypesMetricMetadataDTO;
/**
* @type string
*/

View File

@@ -191,9 +191,6 @@ function TimeSeries({
if (metrics[0] && yAxisUnit) {
updateMetricMetadata(
{
pathParams: {
metricName: metricNames[0],
},
data: buildUpdateMetricYAxisUnitPayload(
metricNames[0],
metrics[0],

View File

@@ -48,18 +48,14 @@ function AllAttributes({
isLoading: isLoadingAttributes,
isError: isErrorAttributes,
refetch: refetchAttributes,
} = useGetMetricAttributes(
{
metricName,
},
{
start: minTime ? Math.floor(minTime / 1000000) : undefined,
end: maxTime ? Math.floor(maxTime / 1000000) : undefined,
},
);
} = useGetMetricAttributes({
metricName,
start: minTime ? Math.floor(minTime / 1000000) : undefined,
end: maxTime ? Math.floor(maxTime / 1000000) : undefined,
});
const attributes = useMemo(
() => attributesData?.data?.attributes ?? [],
() => attributesData?.data.attributes ?? [],
[attributesData],
);

View File

@@ -237,9 +237,6 @@ function Metadata({
const handleSave = useCallback(() => {
updateMetricMetadata(
{
pathParams: {
metricName,
},
data: transformUpdateMetricMetadataRequest(metricName, metricMetadataState),
},
{

View File

@@ -56,7 +56,7 @@ function MetricDetails({
);
const metadata = useMemo(() => {
if (!metricMetadataResponse?.data) {
if (!metricMetadataResponse) {
return null;
}
const { type, description, unit, temporality, isMonotonic } =

View File

@@ -195,14 +195,12 @@ describe('Metadata', () => {
expect(mockUseUpdateMetricMetadata).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
metricName: MOCK_METRIC_NAME,
type: MetrictypesTypeDTO.sum,
temporality: MetrictypesTemporalityDTO.cumulative,
unit: 'By',
isMonotonic: true,
}),
pathParams: {
metricName: MOCK_METRIC_NAME,
},
}),
expect.objectContaining({
onSuccess: expect.any(Function),

View File

@@ -1,4 +1,5 @@
import type {
CoretypesKindDTO,
CoretypesObjectGroupDTO,
CoretypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
@@ -56,7 +57,7 @@ const baseAuthzResources: AuthzResources = {
// API payload resource refs — only kind+type, no allowedVerbs (matches CoretypesResourceRefDTO shape)
const dashboardResourceRef = {
kind: 'dashboard',
kind: 'dashboard' as CoretypesKindDTO,
type: 'metaresource' as CoretypesTypeDTO,
};
const alertResourceRef = {

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Badge } from '@signozhq/ui/badge';
import type {
CoretypesKindDTO,
CoretypesObjectGroupDTO,
CoretypesResourceRefDTO,
CoretypesTypeDTO,
@@ -147,7 +148,7 @@ export function buildPatchPayload({
continue;
}
const resourceDef: CoretypesResourceRefDTO = {
kind: found.kind,
kind: found.kind as CoretypesKindDTO,
type: found.type as CoretypesTypeDTO,
};

View File

@@ -2,13 +2,13 @@ import {
AuthtypesTransactionDTO,
CoretypesTypeDTO,
AuthtypesRelationDTO,
CoretypesKindDTO,
} from '../../api/generated/services/sigNoz.schemas';
import permissionsType from './permissions.type';
import {
AuthZObject,
AuthZRelation,
BrandedPermission,
ResourceName,
ResourcesForRelation,
ResourceType,
} from './types';
@@ -87,7 +87,7 @@ export function permissionToTransactionDto(
relation: relation as AuthtypesRelationDTO,
object: {
resource: {
kind: resourceName as ResourceName,
kind: resourceName as CoretypesKindDTO,
type: type as CoretypesTypeDTO,
},
selector: selector || '*',

View File

@@ -0,0 +1,117 @@
import { useCallback, useEffect, useRef, useState } from 'react';
interface UseInlineOverflowCountOptions {
itemCount: number;
/** Horizontal gap between items, in px. */
gap?: number;
/** Width kept free at the end of the line for a trailing "+N" trigger, in px. */
reserveWidth?: number;
/** Pause measuring (e.g. while expanded) without unmounting. */
enabled?: boolean;
}
interface UseInlineOverflowCountResult {
containerRef: React.RefObject<HTMLDivElement>;
visibleCount: number;
overflowCount: number;
}
/**
* Measures how many of a container's children (each marked
* `data-overflow-item="true"`) fit on a single line, reserving `reserveWidth`
* for a trailing "+N" trigger. Item widths are cached, so children hidden with
* `display: none` still count toward the fit; measuring pauses while `enabled`
* is false.
*/
export function useInlineOverflowCount({
itemCount,
gap = 8,
reserveWidth = 0,
enabled = true,
}: UseInlineOverflowCountOptions): UseInlineOverflowCountResult {
const containerRef = useRef<HTMLDivElement>(null);
const [visibleCount, setVisibleCount] = useState(itemCount);
const itemWidthsRef = useRef<number[]>([]);
const enabledRef = useRef(enabled);
enabledRef.current = enabled;
useEffect(() => {
itemWidthsRef.current = [];
setVisibleCount(itemCount);
}, [itemCount]);
const measure = useCallback((): void => {
const container = containerRef.current;
if (!container || !enabledRef.current) {
return;
}
const itemElements = Array.from(container.children).filter(
(itemElement): itemElement is HTMLElement =>
itemElement instanceof HTMLElement &&
itemElement.dataset.overflowItem === 'true',
);
if (itemElements.length === 0) {
setVisibleCount(0);
return;
}
itemElements.forEach((itemElement, index) => {
if (itemElement.offsetWidth > 0) {
itemWidthsRef.current[index] = itemElement.offsetWidth;
}
});
const cachedWidths: number[] = [];
for (let index = 0; index < itemElements.length; index += 1) {
const cachedWidth = itemWidthsRef.current[index];
if (cachedWidth == null) {
// Width not cached yet — reveal everything for one frame so it gets
// measured, then the next pass collapses accurately.
setVisibleCount(itemElements.length);
return;
}
cachedWidths.push(cachedWidth);
}
const containerWidth = container.clientWidth;
const totalWidth = cachedWidths.reduce(
(runningTotal, itemWidth, index) =>
runningTotal + itemWidth + (index > 0 ? gap : 0),
0,
);
if (totalWidth <= containerWidth) {
setVisibleCount(itemElements.length);
return;
}
const availableWidth = containerWidth - reserveWidth;
let usedWidth = 0;
let fitCount = 0;
for (let index = 0; index < cachedWidths.length; index += 1) {
const itemWidthWithGap = cachedWidths[index] + (index > 0 ? gap : 0);
if (usedWidth + itemWidthWithGap > availableWidth && fitCount > 0) {
break;
}
usedWidth += itemWidthWithGap;
fitCount += 1;
}
setVisibleCount(Math.max(1, fitCount));
}, [gap, reserveWidth]);
useEffect(() => {
const container = containerRef.current;
if (!container) {
return undefined;
}
const observer = new ResizeObserver(() => measure());
observer.observe(container);
Array.from(container.children).forEach((child) => observer.observe(child));
measure();
return (): void => observer.disconnect();
}, [measure, itemCount, enabled]);
return {
containerRef,
visibleCount,
overflowCount: Math.max(0, itemCount - visibleCount),
};
}

View File

@@ -1,11 +1,7 @@
.dashboardActionsContainer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
.dashboardActionsSecondary {
display: flex;
gap: 12px;
}

View File

@@ -1,32 +1,42 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { FullScreenHandle } from 'react-full-screen';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import {
Braces,
ClipboardCopy,
Configure,
Ellipsis,
Copy,
FileJson,
Fullscreen,
Grid3X3,
LockKeyhole,
PenLine,
Plus,
SquareStack,
Trash2,
} from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import type { MenuItem } from '@signozhq/ui/dropdown-menu';
import { toast } from '@signozhq/ui/sonner';
import { cloneDashboardV2 } from 'api/generated/services/dashboard';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { USER_ROLES } from 'types/roles';
import ConfirmDeleteDialog from '../../components/ConfirmDeleteDialog/ConfirmDeleteDialog';
import DashboardSettings from '../../DashboardSettings';
import { useAddSection } from '../../PanelsAndSectionsLayout/Section/hooks/useAddSection';
import SectionTitleModal from '../../PanelsAndSectionsLayout/Section/SectionTitleModal';
import JsonEditorDrawer from '../JsonEditorDrawer/JsonEditorDrawer';
import SettingsDrawer from '../SettingsDrawer';
import styles from './DashboardActions.module.scss';
import { useDashboardStore } from '../../store/useDashboardStore';
@@ -55,14 +65,31 @@ function DashboardActions({
const canEdit = useDashboardStore((s) => s.isEditable);
const { user } = useAppContext();
const { t } = useTranslation(['dashboard', 'common']);
const { safeNavigate } = useSafeNavigate();
const { showErrorModal } = useErrorModal();
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] =
useState<boolean>(false);
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState<boolean>(false);
const [isCloning, setIsCloning] = useState<boolean>(false);
const [isNewSectionOpen, setIsNewSectionOpen] = useState<boolean>(false);
const [state, setCopy] = useCopyToClipboard();
const [isDeleteOpen, setIsDeleteOpen] = useState<boolean>(false);
const deleteDashboardMutation = useDeleteDashboard(dashboard.id);
const { addSection, isSaving: isAddingSection } = useAddSection({
layouts: dashboard.spec.layouts,
});
const handleCreateSection = useCallback(
async (title: string): Promise<void> => {
await addSection(title);
setIsNewSectionOpen(false);
},
[addSection],
);
useEffect(() => {
if (state.error) {
toast.error(t('something_went_wrong', { ns: 'common' }));
@@ -89,6 +116,24 @@ function DashboardActions({
URL.revokeObjectURL(url);
}, [dashboardDataJSON, title]);
const handleClone = useCallback(async (): Promise<void> => {
if (!dashboard.id) {
return;
}
try {
setIsCloning(true);
const response = await cloneDashboardV2({ id: dashboard.id });
toast.success('Dashboard cloned');
safeNavigate(
generatePath(ROUTES.DASHBOARD, { dashboardId: response.data.id }),
);
} catch (error) {
showErrorModal(error as APIError);
} finally {
setIsCloning(false);
}
}, [dashboard.id, safeNavigate, showErrorModal]);
const handleConfirmDelete = useCallback((): void => {
deleteDashboardMutation.mutate(undefined, {
onSuccess: () => {
@@ -99,17 +144,24 @@ function DashboardActions({
}, [deleteDashboardMutation]);
const menuItems = useMemo<MenuItem[]>(() => {
const editGroup: MenuItem[] = [];
const dashboardGroup: MenuItem[] = [];
if (canEdit) {
editGroup.push({
dashboardGroup.push({
key: 'rename',
label: 'Rename',
icon: <PenLine size={14} />,
onClick: onOpenRename,
});
}
dashboardGroup.push({
key: 'clone',
label: 'Clone dashboard',
icon: <Copy size={14} />,
disabled: isCloning,
onClick: (): void => void handleClone(),
});
if (isAuthor || user.role === USER_ROLES.ADMIN) {
editGroup.push({
dashboardGroup.push({
key: 'lock',
label: isDashboardLocked ? 'Unlock dashboard' : 'Lock dashboard',
icon: <LockKeyhole size={14} />,
@@ -117,14 +169,14 @@ function DashboardActions({
onClick: onLockToggle,
});
}
editGroup.push({
dashboardGroup.push({
key: 'fullscreen',
label: 'Full screen',
icon: <Fullscreen size={14} />,
onClick: handle.enter,
});
const exportGroup: MenuItem[] = [
const dataGroup: MenuItem[] = [
{
key: 'export',
label: 'Export JSON',
@@ -139,7 +191,35 @@ function DashboardActions({
},
];
const dangerGroup: MenuItem[] = [
const layoutGroup: MenuItem[] = [];
if (canEdit) {
layoutGroup.push({
key: 'new-section',
label: 'New section',
icon: <SquareStack size={14} />,
onClick: (): void => setIsNewSectionOpen(true),
});
}
const items: MenuItem[] = [
{
type: 'group',
key: 'group-dashboard',
label: 'Dashboard',
children: dashboardGroup,
},
{ type: 'group', key: 'group-data', label: 'Data', children: dataGroup },
];
if (layoutGroup.length > 0) {
items.push({
type: 'group',
key: 'group-layout',
label: 'Layout',
children: layoutGroup,
});
}
items.push(
{ type: 'divider', key: 'divider-danger' },
{
key: 'delete',
label: 'Delete dashboard',
@@ -147,74 +227,85 @@ function DashboardActions({
danger: true,
onClick: (): void => setIsDeleteOpen(true),
},
];
return [editGroup, exportGroup, dangerGroup]
.filter((group) => group.length > 0)
.flatMap((group, index) =>
index > 0 ? [{ type: 'divider' } as MenuItem, ...group] : group,
);
);
return items;
}, [
isDashboardLocked,
canEdit,
isCloning,
isAuthor,
user.role,
isDashboardLocked,
dashboard.createdBy,
onOpenRename,
handleClone,
onLockToggle,
handle.enter,
exportJSON,
setCopy,
dashboardDataJSON,
canEdit,
]);
return (
<div className={styles.dashboardActionsContainer}>
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
<div className={styles.dashboardActionsSecondary}>
<DropdownMenuSimple menu={{ items: menuItems }}>
<DropdownMenuSimple menu={{ items: menuItems }}>
<Button
variant="solid"
color="secondary"
size="md"
prefix={<Grid3X3 size="md" />}
testId="options"
>
Actions
</Button>
</DropdownMenuSimple>
{canEdit && (
<>
<Button
variant="solid"
color="secondary"
size="icon"
prefix={<Ellipsis size="md" />}
testId="options"
/>
</DropdownMenuSimple>
{canEdit && (
<>
<Button
variant="solid"
color="secondary"
prefix={<Configure size="md" />}
testId="show-drawer"
onClick={(): void => setIsSettingsDrawerOpen(true)}
size="md"
>
Configure
</Button>
<SettingsDrawer
drawerTitle="Dashboard Configuration"
isOpen={isSettingsDrawerOpen}
onClose={(): void => setIsSettingsDrawerOpen(false)}
>
<DashboardSettings dashboard={dashboard} />
</SettingsDrawer>
</>
)}
{!isDashboardLocked && (
<Button
variant="solid"
color="primary"
onClick={onAddPanel}
prefix={<Plus size="md" />}
testId="add-panel-header"
prefix={<Configure size="md" />}
testId="show-drawer"
onClick={(): void => setIsSettingsDrawerOpen(true)}
size="md"
>
New Panel
Configure
</Button>
)}
</div>
<SettingsDrawer
drawerTitle="Dashboard Configuration"
isOpen={isSettingsDrawerOpen}
onClose={(): void => setIsSettingsDrawerOpen(false)}
>
<DashboardSettings dashboard={dashboard} />
</SettingsDrawer>
</>
)}
<Button
variant="solid"
color="secondary"
prefix={<Braces size="md" />}
testId="edit-json"
onClick={(): void => setIsJsonEditorOpen(true)}
size="md"
>
Edit as JSON
</Button>
{!isDashboardLocked && (
<Button
variant="solid"
color="primary"
onClick={onAddPanel}
prefix={<Plus size="md" />}
testId="add-panel-header"
size="md"
>
New Panel
</Button>
)}
<JsonEditorDrawer
dashboard={dashboard}
isOpen={isJsonEditorOpen}
onClose={(): void => setIsJsonEditorOpen(false)}
/>
<ConfirmDeleteDialog
open={isDeleteOpen}
title={`Delete dashboard"?`}
@@ -223,6 +314,15 @@ function DashboardActions({
onConfirm={handleConfirmDelete}
onClose={(): void => setIsDeleteOpen(false)}
/>
<SectionTitleModal
open={isNewSectionOpen}
heading="New section"
okText="Create section"
initialValue=""
isSaving={isAddingSection}
onClose={(): void => setIsNewSectionOpen(false)}
onSubmit={handleCreateSection}
/>
</div>
);
}

View File

@@ -1,19 +1,9 @@
.dashboardInfo {
display: flex;
flex-direction: column;
gap: 8px;
width: 40%;
@media (min-width: 1280px) {
width: 30%;
}
}
.dashboardTitleContainer {
display: flex;
flex: 1;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
}
.dashboardImage {
@@ -21,9 +11,8 @@
}
.dashboardTitle {
flex: 1;
flex: 0 1 auto;
min-width: 0;
max-width: fit-content;
color: var(--l1-foreground);
font-size: 18px;
font-weight: 500;
@@ -37,6 +26,19 @@
cursor: text !important;
}
.descriptionIcon {
flex-shrink: 0;
color: var(--l2-foreground);
cursor: help;
}
.divider {
flex-shrink: 0;
width: 1px;
height: 18px;
background: var(--l2-border);
}
.dashboardTitleEditor {
display: flex;
align-items: center;
@@ -54,8 +56,13 @@
flex-shrink: 0;
}
/* Flexes into the remaining space and clips so the ResizeObserver can measure
how many tags fit before collapsing the rest into a `+N` badge. */
.dashboardTags {
display: flex;
flex-wrap: wrap;
gap: 8px;
flex: 1 1 0;
align-items: center;
gap: 4px;
min-width: 0;
overflow: hidden;
}

View File

@@ -1,5 +1,5 @@
import { KeyboardEvent } from 'react';
import { Check, Globe, LockKeyhole, X } from '@signozhq/icons';
import { Check, Globe, LockKeyhole, SolidInfoCircle, X } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
@@ -9,6 +9,7 @@ import cx from 'classnames';
import { isEmpty } from 'lodash-es';
import styles from './DashboardInfo.module.scss';
import { useVisibleTagCount } from './useVisibleTagCount';
import { useDashboardStore } from '../../store/useDashboardStore';
interface DashboardInfoProps {
@@ -45,6 +46,11 @@ function DashboardInfo({
const hasTags = tags.length > 0;
const hasDescription = !isEmpty(description);
const { containerRef, visibleCount } = useVisibleTagCount(tags);
const needsOverflow = tags.length > visibleCount;
const visibleTags = needsOverflow ? tags.slice(0, visibleCount) : tags;
const remainingTags = needsOverflow ? tags.slice(visibleCount) : [];
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
if (event.key === 'Enter') {
event.preventDefault();
@@ -56,83 +62,106 @@ function DashboardInfo({
return (
<div className={styles.dashboardInfo}>
<div className={styles.dashboardTitleContainer}>
<img src={image} alt={title} className={styles.dashboardImage} />
{isEditing ? (
<div className={styles.dashboardTitleEditor}>
<Input
autoFocus
value={draft}
testId="dashboard-title-input"
maxLength={120}
className={styles.dashboardTitleInput}
onChange={(e): void => onDraftChange(e.target.value)}
onKeyDown={onKeyDown}
/>
<Button
type="button"
variant="outlined"
color="primary"
size="icon"
className={styles.dashboardTitleActionButton}
aria-label="Save title"
testId="dashboard-title-save"
onClick={onCommit}
>
<Check size={14} />
</Button>
<Button
type="button"
variant="outlined"
color="secondary"
size="icon"
className={styles.dashboardTitleActionButton}
aria-label="Cancel title edit"
testId="dashboard-title-cancel"
onClick={onCancel}
>
<X size={14} />
</Button>
</div>
) : (
<TooltipSimple title={title}>
<Typography.Text
className={cx(styles.dashboardTitle, {
[styles.dashboardTitleHover]: canEdit,
})}
data-testid="dashboard-title"
onClick={canEdit ? onStartEdit : undefined}
>
{title}
</Typography.Text>
</TooltipSimple>
)}
<img src={image} alt={title} className={styles.dashboardImage} />
{isPublicDashboard && (
<TooltipSimple title="This dashboard is publicly accessible">
<Globe size={14} />
</TooltipSimple>
)}
{isDashboardLocked && (
<TooltipSimple title="This dashboard is locked">
<LockKeyhole size={14} />
</TooltipSimple>
)}
</div>
{hasTags && (
<div className={styles.dashboardTags}>
{tags.map((tag) => (
<Badge key={tag} color="warning" variant="outline">
{tag}
</Badge>
))}
{isEditing ? (
<div className={styles.dashboardTitleEditor}>
<Input
autoFocus
value={draft}
testId="dashboard-title-input"
maxLength={120}
className={styles.dashboardTitleInput}
onChange={(e): void => onDraftChange(e.target.value)}
onKeyDown={onKeyDown}
/>
<Button
type="button"
variant="outlined"
color="primary"
size="icon"
className={styles.dashboardTitleActionButton}
aria-label="Save title"
testId="dashboard-title-save"
onClick={onCommit}
>
<Check size={14} />
</Button>
<Button
type="button"
variant="outlined"
color="secondary"
size="icon"
className={styles.dashboardTitleActionButton}
aria-label="Cancel title edit"
testId="dashboard-title-cancel"
onClick={onCancel}
>
<X size={14} />
</Button>
</div>
) : (
<TooltipSimple title={title}>
<Typography.Text
className={cx(styles.dashboardTitle, {
[styles.dashboardTitleHover]: canEdit,
})}
data-testid="dashboard-title"
onClick={canEdit ? onStartEdit : undefined}
>
{title}
</Typography.Text>
</TooltipSimple>
)}
{hasDescription && (
<Typography.Text color="muted">{description}</Typography.Text>
<TooltipSimple title={description}>
<SolidInfoCircle
className={styles.descriptionIcon}
size={14}
data-testid="dashboard-description-info"
/>
</TooltipSimple>
)}
{isPublicDashboard && (
<TooltipSimple title="This dashboard is publicly accessible">
<Globe size={14} />
</TooltipSimple>
)}
{isDashboardLocked && (
<TooltipSimple title="This dashboard is locked">
<LockKeyhole size={14} />
</TooltipSimple>
)}
{hasTags && (
<>
<span className={styles.divider} />
<div
ref={containerRef}
className={styles.dashboardTags}
data-testid="dashboard-tags"
>
{visibleTags.map((tag) => (
<Badge key={tag} color="warning" variant="outline">
{tag}
</Badge>
))}
{remainingTags.length > 0 && (
<TooltipSimple title={remainingTags.join(', ')}>
<Badge
color="warning"
variant="outline"
data-testid="dashboard-tags-overflow"
>
+{remainingTags.length}
</Badge>
</TooltipSimple>
)}
</div>
</>
)}
</div>
);

View File

@@ -0,0 +1,62 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import {
BADGE_GAP,
estimateBadgeWidth,
OVERFLOW_BADGE_WIDTH,
} from 'components/Alerts/LabelColumn/utils';
interface Result {
containerRef: React.RefObject<HTMLDivElement>;
visibleCount: number;
}
/**
* Measures how many tags fit in the container and returns the visible count,
* reserving room for the `+N` overflow badge. Reuses the badge-width estimation
* from the alerts LabelColumn so dashboards and alerts overflow identically.
*/
export function useVisibleTagCount(tags: string[]): Result {
const containerRef = useRef<HTMLDivElement>(null);
const [visibleCount, setVisibleCount] = useState(tags.length);
const calculateVisible = useCallback(
(width: number): number => {
if (width <= 0) {
return 1;
}
const availableWidth = width - OVERFLOW_BADGE_WIDTH - BADGE_GAP;
let usedWidth = 0;
let count = 0;
for (const tag of tags) {
const badgeWidth = estimateBadgeWidth(tag) + BADGE_GAP;
if (usedWidth + badgeWidth > availableWidth && count > 0) {
break;
}
usedWidth += badgeWidth;
count += 1;
}
return Math.max(1, count);
},
[tags],
);
useEffect(() => {
const container = containerRef.current;
if (!container) {
return undefined;
}
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0) {
setVisibleCount(calculateVisible(entry.contentRect.width));
}
});
observer.observe(container);
if (container.clientWidth > 0) {
setVisibleCount(calculateVisible(container.clientWidth));
}
return (): void => observer.disconnect();
}, [calculateVisible]);
return { containerRef, visibleCount };
}

View File

@@ -5,7 +5,9 @@
color: var(--l2-foreground);
background-color: var(--l1-background);
padding: 16px;
box-shadow: 0 2px 2px 0px var(--l2-border);
box-shadow:
0 1px 0 0 var(--l2-border),
0 6px 12px -10px var(--l2-border);
}
.dashboardPageToolbarSubContainer {
@@ -16,5 +18,22 @@
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
width: 100%;
}
.toolbarRow2 {
width: 100%;
margin-top: 12px;
&::after {
display: block;
clear: both;
content: '';
}
}
.timeCluster {
float: right;
margin: 0 0 0 16px;
}

View File

@@ -0,0 +1,72 @@
.root {
:global(.ant-drawer-wrapper-body) {
border-left: 1px solid var(--l1-border);
background: var(--l2-background);
}
:global(.ant-drawer-header) {
height: 48px;
border-bottom: 1px solid var(--l1-border);
:global(.ant-drawer-title) {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-weight: 500;
}
}
:global(.ant-drawer-body) {
padding: 0;
display: flex;
min-height: 0;
}
:global(.ant-drawer-footer) {
padding: 12px 16px;
border-top: 1px solid var(--l1-border);
}
}
.body {
display: flex;
flex: 1;
min-width: 0;
min-height: 0;
flex-direction: column;
}
.editor {
flex: 1;
min-height: 0;
}
.footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.validation {
min-width: 0;
overflow: hidden;
font-family: 'Space Mono', monospace;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.validationValid {
color: var(--bg-forest-400);
}
.validationInvalid {
color: var(--bg-cherry-400);
}
.footerActions {
display: flex;
flex-shrink: 0;
gap: 8px;
}

View File

@@ -0,0 +1,141 @@
import { KeyboardEvent, useCallback } from 'react';
import MEditor from '@monaco-editor/react';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { Drawer } from 'antd';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import { useCopyToClipboard } from 'react-use';
import { toast } from '@signozhq/ui/sonner';
import { defineJsonEditorTheme, JSON_EDITOR_THEME } from './editorTheme';
import styles from './JsonEditorDrawer.module.scss';
import JsonEditorToolbar from './JsonEditorToolbar';
import { useJsonEditor } from './useJsonEditor';
interface JsonEditorDrawerProps {
dashboard: DashboardtypesGettableDashboardV2DTO;
isOpen: boolean;
onClose: () => void;
}
function JsonEditorDrawer({
dashboard,
isOpen,
onClose,
}: JsonEditorDrawerProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const { draft, setDraft, validity, isDirty, isSaving, format, reset, apply } =
useJsonEditor({ dashboard, isOpen, onApplied: onClose });
const onCopy = useCallback((): void => {
copyToClipboard(draft);
toast.success('JSON copied to clipboard');
}, [copyToClipboard, draft]);
const onDownload = useCallback((): void => {
const blob = new Blob([draft], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${dashboard.name || 'dashboard'}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, [draft, dashboard.name]);
const onKeyDown = useCallback(
(event: KeyboardEvent<HTMLDivElement>): void => {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
event.preventDefault();
void apply();
}
},
[apply],
);
const applyDisabled = !isDirty || !validity.valid || isSaving;
const validationText = validity.valid
? `Valid JSON · ${validity.lineCount} lines`
: `Line ${validity.errorLine ?? '?'} · ${validity.message ?? 'Invalid JSON'}`;
return (
<Drawer
title="Dashboard JSON"
placement="right"
width={660}
onClose={onClose}
open={isOpen}
rootClassName={styles.root}
footer={
<div className={styles.footer}>
<Typography.Text
className={cx(styles.validation, {
[styles.validationValid]: validity.valid,
[styles.validationInvalid]: !validity.valid,
})}
data-testid="json-editor-validation"
>
{validationText}
</Typography.Text>
<div className={styles.footerActions}>
<Button
variant="outlined"
color="secondary"
size="md"
testId="json-editor-cancel"
onClick={onClose}
>
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="md"
testId="json-editor-apply"
disabled={applyDisabled}
onClick={(): void => void apply()}
>
Apply changes
</Button>
</div>
</div>
}
>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div className={styles.body} onKeyDown={onKeyDown}>
<JsonEditorToolbar
isDirty={isDirty}
onFormat={format}
onCopy={onCopy}
onDownload={onDownload}
onReset={reset}
/>
<div className={styles.editor}>
<MEditor
language="json"
height="100%"
value={draft}
onChange={(value): void => setDraft(value ?? '')}
options={{
scrollbar: { alwaysConsumeMouseWheel: false },
minimap: { enabled: false },
fontSize: 13,
fontFamily: 'Space Mono',
}}
theme="vs-dark"
onMount={(editor, monaco): void => {
defineJsonEditorTheme(monaco, editor.getContainerDomNode());
monaco.editor.setTheme(JSON_EDITOR_THEME);
void document.fonts.ready.then(() => monaco.editor.remeasureFonts());
}}
/>
</div>
</div>
</Drawer>
);
}
export default JsonEditorDrawer;

View File

@@ -0,0 +1,12 @@
.toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
border-bottom: 1px solid var(--l1-border);
background: var(--l1-background);
}
.spacer {
flex: 1;
}

View File

@@ -0,0 +1,69 @@
import { AlignLeft, Copy, Download, RotateCcw } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import styles from './JsonEditorToolbar.module.scss';
interface JsonEditorToolbarProps {
isDirty: boolean;
onFormat: () => void;
onCopy: () => void;
onDownload: () => void;
onReset: () => void;
}
function JsonEditorToolbar({
isDirty,
onFormat,
onCopy,
onDownload,
onReset,
}: JsonEditorToolbarProps): JSX.Element {
return (
<div className={styles.toolbar}>
<Button
variant="ghost"
color="secondary"
size="sm"
prefix={<AlignLeft size={14} />}
testId="json-editor-format"
onClick={onFormat}
>
Format
</Button>
<Button
variant="ghost"
color="secondary"
size="sm"
prefix={<Copy size={14} />}
testId="json-editor-copy"
onClick={onCopy}
>
Copy
</Button>
<Button
variant="ghost"
color="secondary"
size="sm"
prefix={<Download size={14} />}
testId="json-editor-download"
onClick={onDownload}
>
Download
</Button>
<div className={styles.spacer} />
<Button
variant="ghost"
color="secondary"
size="sm"
prefix={<RotateCcw size={14} />}
testId="json-editor-reset"
disabled={!isDirty}
onClick={onReset}
>
Reset
</Button>
</div>
);
}
export default JsonEditorToolbar;

View File

@@ -0,0 +1,165 @@
import { fireEvent, render, screen } from '@testing-library/react';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import JsonEditorDrawer from '../JsonEditorDrawer';
import { useJsonEditor } from '../useJsonEditor';
jest.mock('../useJsonEditor', () => ({ useJsonEditor: jest.fn() }));
jest.mock('@monaco-editor/react', () => ({
__esModule: true,
default: ({
value,
onChange,
}: {
value: string;
onChange: (next?: string) => void;
}): JSX.Element => (
<textarea
aria-label="json-editor"
data-testid="monaco"
value={value}
onChange={(e): void => onChange(e.target.value)}
/>
),
}));
jest.mock('@signozhq/ui/sonner', () => ({
toast: { success: jest.fn(), error: jest.fn() },
}));
jest.mock('react-use', () => ({
useCopyToClipboard: (): [unknown, jest.Mock] => [{}, jest.fn()],
}));
const mockUseJsonEditor = useJsonEditor as jest.Mock;
const dashboard = {
id: 'dash-1',
name: 'My dashboard',
} as unknown as DashboardtypesGettableDashboardV2DTO;
function hookValue(
overrides: Partial<ReturnType<typeof useJsonEditor>> = {},
): ReturnType<typeof useJsonEditor> {
return {
draft: '{\n "a": 1\n}',
setDraft: jest.fn(),
validity: { valid: true, lineCount: 3 },
isDirty: true,
isSaving: false,
format: jest.fn(),
reset: jest.fn(),
apply: jest.fn().mockResolvedValue(undefined),
...overrides,
};
}
describe('JsonEditorDrawer', () => {
beforeEach(() => jest.clearAllMocks());
it('renders the toolbar, editor and footer actions when open', () => {
mockUseJsonEditor.mockReturnValue(hookValue());
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
expect(screen.getByTestId('json-editor-format')).toBeInTheDocument();
expect(screen.getByTestId('json-editor-copy')).toBeInTheDocument();
expect(screen.getByTestId('json-editor-download')).toBeInTheDocument();
expect(screen.getByTestId('json-editor-reset')).toBeInTheDocument();
expect(screen.getByTestId('json-editor-apply')).toBeInTheDocument();
expect(screen.getByTestId('monaco')).toBeInTheDocument();
});
it('shows a valid status with the line count', () => {
mockUseJsonEditor.mockReturnValue(
hookValue({ validity: { valid: true, lineCount: 12 } }),
);
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
expect(screen.getByTestId('json-editor-validation')).toHaveTextContent(
'Valid JSON · 12 lines',
);
});
it('shows the error line and message when invalid', () => {
mockUseJsonEditor.mockReturnValue(
hookValue({
validity: {
valid: false,
lineCount: 4,
errorLine: 3,
message: 'Unexpected token',
},
}),
);
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
expect(screen.getByTestId('json-editor-validation')).toHaveTextContent(
'Line 3 · Unexpected token',
);
});
it('disables Apply when not dirty, invalid, or saving', () => {
mockUseJsonEditor.mockReturnValue(hookValue({ isDirty: false }));
const { rerender } = render(
<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />,
);
expect(screen.getByTestId('json-editor-apply')).toBeDisabled();
mockUseJsonEditor.mockReturnValue(
hookValue({ validity: { valid: false, lineCount: 1 } }),
);
rerender(
<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />,
);
expect(screen.getByTestId('json-editor-apply')).toBeDisabled();
mockUseJsonEditor.mockReturnValue(hookValue({ isSaving: true }));
rerender(
<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />,
);
expect(screen.getByTestId('json-editor-apply')).toBeDisabled();
});
it('wires toolbar and footer buttons to the hook callbacks', () => {
const value = hookValue();
mockUseJsonEditor.mockReturnValue(value);
const onClose = jest.fn();
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={onClose} />);
fireEvent.click(screen.getByTestId('json-editor-format'));
expect(value.format).toHaveBeenCalled();
fireEvent.click(screen.getByTestId('json-editor-reset'));
expect(value.reset).toHaveBeenCalled();
fireEvent.click(screen.getByTestId('json-editor-apply'));
expect(value.apply).toHaveBeenCalled();
fireEvent.click(screen.getByTestId('json-editor-cancel'));
expect(onClose).toHaveBeenCalled();
});
it('forwards editor changes to setDraft', () => {
const value = hookValue();
mockUseJsonEditor.mockReturnValue(value);
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
fireEvent.change(screen.getByTestId('monaco'), {
target: { value: '{"b":2}' },
});
expect(value.setDraft).toHaveBeenCalledWith('{"b":2}');
});
it('applies on Cmd/Ctrl+Enter', () => {
const value = hookValue();
mockUseJsonEditor.mockReturnValue(value);
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
fireEvent.keyDown(screen.getByTestId('monaco'), {
key: 'Enter',
metaKey: true,
});
expect(value.apply).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,179 @@
import { act, renderHook } from '@testing-library/react';
import { updateDashboardV2 } from 'api/generated/services/dashboard';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import { toast } from '@signozhq/ui/sonner';
import { useJsonEditor } from '../useJsonEditor';
const mockRefetch = jest.fn();
const mockShowErrorModal = jest.fn();
jest.mock('../../../store/useDashboardStore', () => ({
useDashboardStore: (selector: (state: unknown) => unknown): unknown =>
selector({ dashboardId: 'dash-1', refetch: mockRefetch }),
}));
jest.mock('providers/ErrorModalProvider', () => ({
useErrorModal: (): { showErrorModal: jest.Mock } => ({
showErrorModal: mockShowErrorModal,
}),
}));
jest.mock('api/generated/services/dashboard', () => ({
updateDashboardV2: jest.fn(),
}));
jest.mock('@signozhq/ui/sonner', () => ({
toast: { success: jest.fn(), error: jest.fn() },
}));
const mockUpdate = updateDashboardV2 as jest.Mock;
const mockToastSuccess = toast.success as jest.Mock;
const dashboard = {
id: 'dash-1',
name: 'My dashboard',
schemaVersion: 'v6',
image: 'icon.png',
tags: [{ key: 'env', value: 'prod' }],
spec: {
display: { name: 'My dashboard' },
panels: {},
layouts: [],
variables: [],
},
} as unknown as DashboardtypesGettableDashboardV2DTO;
const serialized = JSON.stringify(dashboard, null, 2);
describe('useJsonEditor', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUpdate.mockResolvedValue({});
});
it('seeds the draft from the dashboard and reports valid, non-dirty state', () => {
const { result } = renderHook(() =>
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
);
expect(result.current.draft).toBe(serialized);
expect(result.current.isDirty).toBe(false);
expect(result.current.validity.valid).toBe(true);
expect(result.current.validity.lineCount).toBe(serialized.split('\n').length);
});
it('flags invalid JSON with a line number and marks the draft dirty', () => {
const { result } = renderHook(() =>
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
);
act(() => result.current.setDraft('{\n "name": ,\n}'));
expect(result.current.validity.valid).toBe(false);
expect(result.current.validity.message).toBeDefined();
expect(result.current.isDirty).toBe(true);
});
it('format() pretty-prints valid JSON and leaves invalid JSON untouched', () => {
const { result } = renderHook(() =>
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
);
act(() => result.current.setDraft('{"a":1}'));
act(() => result.current.format());
expect(result.current.draft).toBe('{\n "a": 1\n}');
act(() => result.current.setDraft('{bad'));
act(() => result.current.format());
expect(result.current.draft).toBe('{bad');
});
it('reset() restores the last-applied text', () => {
const { result } = renderHook(() =>
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
);
act(() => result.current.setDraft('edited'));
expect(result.current.isDirty).toBe(true);
act(() => result.current.reset());
expect(result.current.draft).toBe(serialized);
expect(result.current.isDirty).toBe(false);
});
it('apply() is a no-op when the draft is unchanged or invalid', async () => {
const { result } = renderHook(() =>
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
);
await act(async () => {
await result.current.apply();
});
expect(mockUpdate).not.toHaveBeenCalled();
act(() => result.current.setDraft('{bad'));
await act(async () => {
await result.current.apply();
});
expect(mockUpdate).not.toHaveBeenCalled();
});
it('apply() PUTs the narrowed body, toasts, refetches and calls onApplied', async () => {
const onApplied = jest.fn();
const { result } = renderHook(() =>
useJsonEditor({ dashboard, isOpen: true, onApplied }),
);
const next = { ...dashboard, name: 'Renamed' };
act(() => result.current.setDraft(JSON.stringify(next)));
await act(async () => {
await result.current.apply();
});
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(mockUpdate).toHaveBeenCalledWith(
{ id: 'dash-1' },
expect.objectContaining({
name: 'Renamed',
schemaVersion: 'v6',
spec: next.spec,
tags: next.tags,
}),
);
expect(mockToastSuccess).toHaveBeenCalled();
expect(mockRefetch).toHaveBeenCalled();
expect(onApplied).toHaveBeenCalled();
});
it('apply() surfaces errors through the error modal', async () => {
mockUpdate.mockRejectedValueOnce(new Error('boom'));
const { result } = renderHook(() =>
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
);
act(() =>
result.current.setDraft(JSON.stringify({ ...dashboard, name: 'X' })),
);
await act(async () => {
await result.current.apply();
});
expect(mockShowErrorModal).toHaveBeenCalled();
});
it('re-seeds the draft when the drawer re-opens', () => {
const onApplied = jest.fn();
const { result, rerender } = renderHook(
(props: { isOpen: boolean }) =>
useJsonEditor({ dashboard, isOpen: props.isOpen, onApplied }),
{ initialProps: { isOpen: false } },
);
act(() => result.current.setDraft('stale edit'));
expect(result.current.draft).toBe('stale edit');
rerender({ isOpen: true });
expect(result.current.draft).toBe(serialized);
});
});

View File

@@ -0,0 +1,26 @@
import type {
DashboardtypesGettableDashboardV2DTO,
DashboardtypesDashboardSpecDTO,
DashboardtypesUpdatableDashboardV2DTO,
TagtypesPostableTagDTO,
} from 'api/generated/services/sigNoz.schemas';
/**
* Narrow a parsed (full Gettable-shaped) dashboard JSON down to the PUT-updatable
* body. The editor shows the whole dashboard for readability, but the update
* endpoint only accepts `{ name, schemaVersion, image, tags, spec }` — the
* server owns `id`, `locked`, timestamps, etc., so we drop them here.
*/
export function dashboardToUpdatable(
parsed: Record<string, unknown>,
): DashboardtypesUpdatableDashboardV2DTO {
const dashboard = parsed as Partial<DashboardtypesGettableDashboardV2DTO>;
return {
name: dashboard.name ?? '',
schemaVersion: dashboard.schemaVersion ?? 'v6',
image: dashboard.image,
tags: (dashboard.tags as TagtypesPostableTagDTO[] | null | undefined) ?? null,
spec: dashboard.spec as DashboardtypesDashboardSpecDTO,
};
}

View File

@@ -0,0 +1,47 @@
import type { Monaco } from '@monaco-editor/react';
export const JSON_EDITOR_THEME = 'signoz-json';
function token(el: HTMLElement, name: string): string {
return getComputedStyle(el).getPropertyValue(name).trim().replace('#', '');
}
function isDark(hex: string): boolean {
if (hex.length < 6) {
return true;
}
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return 0.299 * r + 0.587 * g + 0.114 * b < 128;
}
export function defineJsonEditorTheme(monaco: Monaco, el: HTMLElement): void {
const background = token(el, '--l1-background');
const foreground = token(el, '--l1-foreground');
const keyColor = token(el, '--bg-vanilla-400');
const valueColor = token(el, '--bg-robin-400');
const rules: { token: string; foreground: string }[] = [];
if (keyColor) {
rules.push({ token: 'string.key.json', foreground: keyColor });
}
if (valueColor) {
rules.push({ token: 'string.value.json', foreground: valueColor });
}
const colors: Record<string, string> = {};
if (background) {
colors['editor.background'] = `#${background}`;
}
if (foreground) {
colors['editor.foreground'] = `#${foreground}`;
}
monaco.editor.defineTheme(JSON_EDITOR_THEME, {
base: isDark(background) ? 'vs-dark' : 'vs',
inherit: true,
rules,
colors,
});
}

View File

@@ -0,0 +1,148 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { toast } from '@signozhq/ui/sonner';
import { updateDashboardV2 } from 'api/generated/services/dashboard';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { dashboardToUpdatable } from './dashboardToUpdatable';
import { useDashboardStore } from '../../store/useDashboardStore';
export interface JsonValidity {
valid: boolean;
lineCount: number;
/** 1-based line of the parse error, when known. */
errorLine?: number;
message?: string;
}
interface Params {
dashboard: DashboardtypesGettableDashboardV2DTO;
isOpen: boolean;
onApplied: () => void;
}
interface Result {
draft: string;
setDraft: (next: string) => void;
validity: JsonValidity;
isDirty: boolean;
isSaving: boolean;
format: () => void;
reset: () => void;
apply: () => Promise<void>;
}
const serialize = (dashboard: DashboardtypesGettableDashboardV2DTO): string =>
JSON.stringify(dashboard, null, 2);
/** Derive a 1-based line number from a `JSON.parse` "position N" error message. */
function errorLineFromMessage(
source: string,
message: string,
): number | undefined {
const match = /position (\d+)/.exec(message);
if (!match) {
return undefined;
}
const position = Number(match[1]);
return source.slice(0, position).split('\n').length;
}
/**
* Editor state for the dashboard JSON drawer: tracks the editable `draft`
* against the last-applied text, exposes live validation, and applies changes
* via the full-document update endpoint.
*/
export function useJsonEditor({
dashboard,
isOpen,
onApplied,
}: Params): Result {
const dashboardId = useDashboardStore((s) => s.dashboardId);
const refetch = useDashboardStore((s) => s.refetch);
const { showErrorModal } = useErrorModal();
const [appliedText, setAppliedText] = useState<string>(() =>
serialize(dashboard),
);
const [draft, setDraft] = useState<string>(appliedText);
const [isSaving, setIsSaving] = useState(false);
// Re-seed the editor from the current dashboard each time the drawer opens so
// it always reflects the latest persisted state (e.g. after a refetch).
useEffect(() => {
if (isOpen) {
const next = serialize(dashboard);
setAppliedText(next);
setDraft(next);
}
}, [isOpen, dashboard]);
const validity = useMemo<JsonValidity>(() => {
const lineCount = draft.split('\n').length;
try {
JSON.parse(draft);
return { valid: true, lineCount };
} catch (error) {
const message = error instanceof Error ? error.message : 'Invalid JSON';
return {
valid: false,
lineCount,
errorLine: errorLineFromMessage(draft, message),
message,
};
}
}, [draft]);
const isDirty = draft !== appliedText;
const format = useCallback((): void => {
try {
setDraft(JSON.stringify(JSON.parse(draft), null, 2));
} catch {
// Leave the draft untouched when it can't be parsed.
}
}, [draft]);
const reset = useCallback((): void => {
setDraft(appliedText);
}, [appliedText]);
const apply = useCallback(async (): Promise<void> => {
if (!validity.valid || !isDirty) {
return;
}
try {
setIsSaving(true);
const parsed = JSON.parse(draft) as Record<string, unknown>;
await updateDashboardV2({ id: dashboardId }, dashboardToUpdatable(parsed));
toast.success('Dashboard updated');
refetch();
onApplied();
} catch (error) {
showErrorModal(error as APIError);
} finally {
setIsSaving(false);
}
}, [
dashboardId,
validity.valid,
isDirty,
draft,
refetch,
onApplied,
showErrorModal,
]);
return {
draft,
setDraft,
validity,
isDirty,
isSaving,
format,
reset,
apply,
};
}

View File

@@ -12,6 +12,7 @@ import type {
DashboardtypesJSONPatchOperationDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useAppContext } from 'providers/App/App';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { useErrorModal } from 'providers/ErrorModalProvider';
@@ -139,7 +140,15 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
/>
</div>
<VariablesBar dashboard={dashboard} />
{/* Row 2: the time selector floats top-right (declared first so the
variables bar's content wraps around it); the variables bar
collapses to one line and, when expanded, wraps full-width under it. */}
<div className={styles.toolbarRow2}>
<div className={styles.timeCluster}>
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
</div>
<VariablesBar dashboard={dashboard} />
</div>
</section>
);
}

View File

@@ -13,7 +13,7 @@ import { useDashboardStore } from '../../../store/useDashboardStore';
import { useDeleteSection } from '../hooks/useDeleteSection';
import { useRenameSection } from '../hooks/useRenameSection';
import { useToggleSectionCollapse } from '../hooks/useToggleSectionCollapse';
import RenameSectionModal from '../RenameSectionModal';
import SectionTitleModal from '../SectionTitleModal';
import SectionGrid from '../SectionGrid/SectionGrid';
import SectionHeader, {
type SectionDragHandle,
@@ -146,8 +146,10 @@ function Section({
)}
</div>
))}
<RenameSectionModal
<SectionTitleModal
open={isRenaming}
heading="Rename section"
okText="Rename"
initialValue={section.title}
isSaving={isSaving}
onClose={(): void => setIsRenaming(false)}

View File

@@ -2,21 +2,30 @@ import { useEffect, useState } from 'react';
import { Modal } from 'antd';
import { Input } from '@signozhq/ui/input';
interface RenameSectionModalProps {
interface SectionTitleModalProps {
open: boolean;
/** Modal heading, e.g. "Rename section" / "New section". */
heading: string;
/** Confirm button label, e.g. "Rename" / "Create section". */
okText: string;
initialValue: string;
isSaving: boolean;
placeholder?: string;
onClose: () => void;
onSubmit: (title: string) => void;
}
function RenameSectionModal({
/** Title-entry modal shared by section create and rename. */
function SectionTitleModal({
open,
heading,
okText,
initialValue,
isSaving,
placeholder = 'Section name',
onClose,
onSubmit,
}: RenameSectionModalProps): JSX.Element {
}: SectionTitleModalProps): JSX.Element {
const [value, setValue] = useState<string>(initialValue);
// Reseed the field each time the modal opens.
@@ -36,19 +45,19 @@ function RenameSectionModal({
return (
<Modal
open={open}
title="Rename section"
title={heading}
onCancel={onClose}
onOk={submit}
okText="Rename"
okText={okText}
okButtonProps={{ disabled: isSaving || !value.trim() }}
destroyOnClose
>
<Input
testId="rename-section-input"
testId="section-title-input"
autoFocus
value={value}
maxLength={120}
placeholder="Section name"
placeholder={placeholder}
onChange={(e): void => setValue(e.target.value)}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
@@ -61,4 +70,4 @@ function RenameSectionModal({
);
}
export default RenameSectionModal;
export default SectionTitleModal;

View File

@@ -12,6 +12,27 @@ import {
} from '../../../patchOps';
import { useDashboardStore } from '../../../store/useDashboardStore';
const SECTION_SELECTOR = '[data-testid^="dashboard-section-"]';
/**
* Waits (via rAF) for the refetch to render the appended section, then scrolls
* it into view. Polls because `refetch` resolves before React commits the new
* section to the DOM; bails after ~40 frames.
*/
function scrollToNewSection(prevCount: number, attempts = 40): void {
const sections = document.querySelectorAll(SECTION_SELECTOR);
if (sections.length > prevCount) {
sections[sections.length - 1]?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
return;
}
if (attempts > 0) {
requestAnimationFrame(() => scrollToNewSection(prevCount, attempts - 1));
}
}
interface Params {
layouts: DashboardtypesLayoutDTO[] | undefined | null;
}
@@ -42,10 +63,12 @@ export function useAddSection({ layouts }: Params): Result {
!layouts || layouts.length === 0
? reorderLayoutsOp([newGridLayout(trimmed)])
: addSectionOp(trimmed);
const prevSectionCount = document.querySelectorAll(SECTION_SELECTOR).length;
try {
setIsSaving(true);
await patchDashboardV2({ id: dashboardId }, [op]);
refetch();
scrollToNewSection(prevSectionCount);
} catch (error) {
showErrorModal(error as APIError);
} finally {

View File

@@ -101,7 +101,7 @@ function VariableSelector({
${variable.name}
{variable.description ? (
<Tooltip title={variable.description}>
<SolidInfoCircle className={styles.infoIcon} size="md" />
<SolidInfoCircle className={styles.infoIcon} size={14} />
</Tooltip>
) : null}
</Typography.Text>

View File

@@ -1,12 +1,55 @@
/* Mirrors the V1 dashboard variable bar: each variable is a connected pill —
a robin `$name` segment joined to a value segment. */
/* Sits inside the already-padded sticky toolbar section, so it only needs a top
gap from the tags — horizontal/bottom padding comes from the toolbar. */
.bar {
min-width: 0;
}
.strip {
display: flow-root;
}
.stripExpanded {
display: flex;
flex-wrap: wrap;
gap: 12px;
gap: 8px;
padding-top: 12px;
overflow: visible;
clear: both;
.variableSlot,
.moreButton {
margin: 0;
}
@media (prefers-reduced-motion: no-preference) {
animation: variablesExpandIn 200ms ease-out;
}
}
@keyframes variablesExpandIn {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.variableSlot {
display: inline-flex;
align-items: center;
margin-right: 8px;
vertical-align: top;
}
.variableSlotHidden {
display: none;
}
.moreButton {
display: inline-flex;
vertical-align: top;
}
.variableItem {
@@ -21,7 +64,7 @@
align-items: center;
gap: 4px;
padding: 6px 6px 6px 8px;
border: 1px solid var(--l1-border);
border: 1px solid var(--l3-border);
border-radius: 2px 0 0 2px;
background: var(--l3-background);
color: var(--bg-robin-300);
@@ -33,8 +76,10 @@
}
.infoIcon {
margin-left: 4px;
display: inline-flex;
margin-left: 2px;
color: var(--l2-foreground);
vertical-align: middle;
}
.variableValue {
@@ -42,7 +87,7 @@
min-width: 120px;
height: 32px;
align-items: center;
border: 1px solid var(--l1-border);
border: 1px solid var(--l3-border);
border-left: none;
border-radius: 0 2px 2px 0;
background: var(--l2-background);
@@ -55,8 +100,6 @@
}
}
/* Inner control fills the value segment; the segment provides the frame, so the
control itself is borderless/transparent. */
.control {
width: 100%;
min-width: 120px;

View File

@@ -1,4 +1,9 @@
import { useState } from 'react';
import { ChevronLeft } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import cx from 'classnames';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import { useInlineOverflowCount } from 'hooks/useInlineOverflowCount';
import { useVariableSelection } from './useVariableSelection';
import VariableSelector from './VariableSelector';
@@ -11,33 +16,76 @@ interface VariablesBarProps {
/**
* Runtime variable selector bar shown above the panels. Renders one control per
* dashboard variable; selections live in the store + URL (never the spec).
*
* The pills sit on the line left of the floated time selector and collapse the
* overflow behind a `+N` trigger. Expanding lets the bar wrap onto full-width
* lines that flow underneath the time selector. Every selector stays mounted
* either way so auto-selection and option fetching keep driving the panels.
*/
function VariablesBar({ dashboard }: VariablesBarProps): JSX.Element | null {
const { variables, dependencyData, selection, setSelection } =
useVariableSelection(dashboard);
const [expanded, setExpanded] = useState(false);
const { containerRef, visibleCount, overflowCount } = useInlineOverflowCount({
itemCount: variables.length,
gap: 8,
reserveWidth: 48,
enabled: !expanded,
});
if (variables.length === 0) {
return null;
}
const hasOverflow = overflowCount > 0;
return (
<div className={styles.bar} data-testid="dashboard-variables-bar">
{variables.map((variable) => (
<VariableSelector
key={variable.name}
variable={variable}
variables={variables}
parents={dependencyData.parentGraph[variable.name] ?? []}
selections={selection}
selection={
selection[variable.name] ?? {
value: variable.multiSelect ? [] : '',
allSelected: false,
}
}
onChange={(next): void => setSelection(variable.name, next)}
/>
))}
<div
ref={containerRef}
className={cx(styles.strip, { [styles.stripExpanded]: expanded })}
>
{variables.map((variable, index) => (
<div
key={variable.name}
data-overflow-item="true"
className={cx(styles.variableSlot, {
[styles.variableSlotHidden]:
!expanded && hasOverflow && index >= visibleCount,
})}
>
<VariableSelector
variable={variable}
variables={variables}
parents={dependencyData.parentGraph[variable.name] ?? []}
selections={selection}
selection={
selection[variable.name] ?? {
value: variable.multiSelect ? [] : '',
allSelected: false,
}
}
onChange={(next): void => setSelection(variable.name, next)}
/>
</div>
))}
{hasOverflow && (
<span className={styles.moreButton}>
<Button
variant="outlined"
color="secondary"
size="md"
prefix={expanded ? <ChevronLeft size={14} /> : undefined}
aria-expanded={expanded}
testId="dashboard-variables-more"
onClick={(): void => setExpanded((prev) => !prev)}
>
{expanded ? 'Less' : `+${overflowCount}`}
</Button>
</span>
)}
</div>
</div>
);
}

View File

@@ -73,7 +73,7 @@ function ValueSelector({
return (
<CustomSelect
className={styles.select}
className={styles.control}
data-testid={testId}
options={optionData}
value={

View File

@@ -48,8 +48,15 @@ export const createVariableSelectionSlice: StateCreator<
},
});
/**
* Stable empty map for dashboards with no stored selections. Returning an inline
* `{}` here would hand zustand's useSyncExternalStore a new reference every call,
* which it reads as a changed snapshot → infinite re-render loop.
*/
const EMPTY_SELECTION_MAP: VariableSelectionMap = {};
/** Selector: the selection map for a dashboard (empty if none). */
export const selectVariableValues =
(dashboardId: string) =>
(state: DashboardStore): VariableSelectionMap =>
state.variableValues[dashboardId] ?? {};
state.variableValues[dashboardId] ?? EMPTY_SELECTION_MAP;

View File

@@ -0,0 +1,133 @@
{
"title": "TransactionGroups",
"items": {
"$ref": "#/definitions/AuthtypesTransactionGroup"
},
"definitions": {
"AuthtypesRelation": {
"additionalProperties": false,
"enum": [
"create",
"read",
"update",
"delete",
"list",
"assignee",
"attach",
"detach"
],
"type": "string"
},
"AuthtypesTransactionGroup": {
"required": [
"relation",
"objectGroup"
],
"properties": {
"objectGroup": {
"$ref": "#/definitions/CoretypesObjectGroup"
},
"relation": {
"$ref": "#/definitions/AuthtypesRelation"
}
},
"type": "object"
},
"CoretypesKind": {
"additionalProperties": false,
"enum": [
"anonymous",
"organization",
"role",
"serviceaccount",
"user",
"notification-channel",
"route-policy",
"apdex-setting",
"auth-domain",
"session",
"cloud-integration",
"cloud-integration-service",
"integration",
"dashboard",
"public-dashboard",
"ingestion-key",
"ingestion-limit",
"pipeline",
"user-preference",
"org-preference",
"quick-filter",
"ttl-setting",
"rule",
"planned-maintenance",
"saved-view",
"trace-funnel",
"factor-password",
"factor-api-key",
"license",
"subscription",
"logs",
"traces",
"metrics",
"audit-logs",
"meter-metrics",
"logs-field",
"traces-field"
],
"type": "string"
},
"CoretypesObjectGroup": {
"required": [
"resource",
"selectors"
],
"additionalProperties": false,
"properties": {
"resource": {
"$ref": "#/definitions/CoretypesResourceRef"
},
"selectors": {
"items": {
"$ref": "#/definitions/CoretypesSelector"
},
"type": "array"
}
},
"type": "object"
},
"CoretypesResourceRef": {
"required": [
"type",
"kind"
],
"additionalProperties": false,
"properties": {
"kind": {
"$ref": "#/definitions/CoretypesKind"
},
"type": {
"$ref": "#/definitions/CoretypesType"
}
},
"type": "object"
},
"CoretypesSelector": {
"additionalProperties": false,
"type": "string"
},
"CoretypesType": {
"additionalProperties": false,
"enum": [
"user",
"serviceaccount",
"anonymous",
"role",
"organization",
"metaresource",
"telemetryresource"
],
"type": "string"
}
},
"type": "array"
}

View File

@@ -1,4 +1,5 @@
{
"title": "WebSettings",
"required": [
"posthog",
"appcues",

View File

@@ -794,6 +794,13 @@ notifications - 2050
background: color-mix(in srgb, var(--l3-background) 20%, transparent);
}
// =================================================================
// Monaco Editor style overrides
.monaco-editor .find-widget.visible {
top: 30px !important;
right: 45px !important;
}
body.ai-assistant-panel-open {
.PylonChat-bubbleFrameContainer,
.PylonChat-chatWindowFrameContainer {

View File

@@ -1,4 +1,4 @@
/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM docs/config/web-settings.json */
/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM frontend/src/schemas/generated/webSettings.schema.json */
export interface WebSettings {
appcues: Appcues;

View File

@@ -16,7 +16,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListHosts",
Tags: []string{"inframonitoring"},
Summary: "List Hosts for Infra Monitoring",
Description: "Returns a paginated list of hosts with key infrastructure metrics: CPU usage (%), memory usage (%), I/O wait (%), disk usage (%), and 15-minute load average. Each host includes its current status (active/inactive based on metrics reported in the last 10 minutes) and metadata attributes (e.g., os.type). Supports filtering via a filter expression, filtering by host status, custom groupBy to aggregate hosts by any attribute, ordering by any of the five metrics, and pagination via offset/limit. The response type is 'list' for the default host.name grouping or 'grouped_list' for custom groupBy keys. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of hosts with key infrastructure metrics: CPU usage (%), memory usage (%), I/O wait (%), disk usage (%), and 15-minute load average. Each host includes its current status (active/inactive based on metrics reported in the last 10 minutes) and metadata attributes (e.g., os.type). Supports filtering via a filter expression, filtering by host status, custom groupBy to aggregate hosts by any attribute, ordering by any of the five metrics, and pagination via offset/limit. The response type is 'list' for the default host.name grouping or 'grouped_list' for custom groupBy keys. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableHosts),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Hosts),
@@ -35,7 +35,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListPods",
Tags: []string{"inframonitoring"},
Summary: "List Pods for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase: { pending, running, succeeded, failed, unknown } derived from each pod's latest phase in the window). Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase: { pending, running, succeeded, failed, unknown } derived from each pod's latest phase in the window). Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostablePods),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Pods),
@@ -54,7 +54,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListNodes",
Tags: []string{"inframonitoring"},
Summary: "List Nodes for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableNodes),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Nodes),
@@ -73,7 +73,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListNamespaces",
Tags: []string{"inframonitoring"},
Summary: "List Namespaces for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableNamespaces),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Namespaces),
@@ -92,7 +92,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListClusters",
Tags: []string{"inframonitoring"},
Summary: "List Clusters for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableClusters),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Clusters),
@@ -111,7 +111,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListVolumes",
Tags: []string{"inframonitoring"},
Summary: "List Volumes for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes persistent volume claims (PVCs) with key volume metrics: available bytes, capacity bytes, usage (capacity - available), inodes, free inodes, and used inodes. Each row also includes metadata attributes (k8s.persistentvolumeclaim.name, k8s.pod.uid, k8s.pod.name, k8s.namespace.name, k8s.node.name, k8s.statefulset.name, k8s.cluster.name). Supports filtering via a filter expression, custom groupBy to aggregate volumes by any attribute, ordering by any of the six metrics (available, capacity, usage, inodes, inodes_free, inodes_used), and pagination via offset/limit. The response type is 'list' for the default k8s.persistentvolumeclaim.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates volumes in the group. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (volumeAvailable, volumeCapacity, volumeUsage, volumeInodes, volumeInodesFree, volumeInodesUsed) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes persistent volume claims (PVCs) with key volume metrics: available bytes, capacity bytes, usage (capacity - available), inodes, free inodes, and used inodes. Each row also includes metadata attributes (k8s.persistentvolumeclaim.name, k8s.pod.uid, k8s.pod.name, k8s.namespace.name, k8s.node.name, k8s.statefulset.name, k8s.cluster.name). Supports filtering via a filter expression, custom groupBy to aggregate volumes by any attribute, ordering by any of the six metrics (available, capacity, usage, inodes, inodes_free, inodes_used), and pagination via offset/limit. The response type is 'list' for the default k8s.persistentvolumeclaim.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates volumes in the group. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (volumeAvailable, volumeCapacity, volumeUsage, volumeInodes, volumeInodesFree, volumeInodesUsed) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableVolumes),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Volumes),
@@ -130,7 +130,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListDeployments",
Tags: []string{"inframonitoring"},
Summary: "List Deployments for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableDeployments),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Deployments),
@@ -149,7 +149,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListStatefulSets",
Tags: []string{"inframonitoring"},
Summary: "List StatefulSets for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableStatefulSets),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.StatefulSets),
@@ -168,7 +168,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListJobs",
Tags: []string{"inframonitoring"},
Summary: "List Jobs for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableJobs),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Jobs),
@@ -187,7 +187,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListDaemonSets",
Tags: []string{"inframonitoring"},
Summary: "List DaemonSets for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableDaemonSets),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.DaemonSets),

View File

@@ -68,7 +68,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/metrics/{metric_name}/attributes", handler.New(
if err := router.Handle("/api/v2/metrics/attributes", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricAttributes),
handler.OpenAPIDef{
ID: "GetMetricAttributes",
@@ -88,7 +88,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/metrics/{metric_name}/metadata", handler.New(
if err := router.Handle("/api/v2/metrics/metadata", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricMetadata),
handler.OpenAPIDef{
ID: "GetMetricMetadata",
@@ -96,6 +96,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
Summary: "Get metric metadata",
Description: "This endpoint returns metadata information like metric description, unit, type, temporality, monotonicity for a specified metric",
Request: nil,
RequestQuery: new(metricsexplorertypes.MetricNameQuery),
RequestContentType: "",
Response: new(metricsexplorertypes.MetricMetadata),
ResponseContentType: "application/json",
@@ -107,7 +108,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/metrics/{metric_name}/metadata", handler.New(
if err := router.Handle("/api/v2/metrics/metadata", handler.New(
provider.authzMiddleware.EditAccess(provider.metricsExplorerHandler.UpdateMetricMetadata),
handler.OpenAPIDef{
ID: "UpdateMetricMetadata",
@@ -126,7 +127,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/metrics/{metric_name}/highlights", handler.New(
if err := router.Handle("/api/v2/metrics/highlights", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricHighlights),
handler.OpenAPIDef{
ID: "GetMetricHighlights",
@@ -134,6 +135,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
Summary: "Get metric highlights",
Description: "This endpoint returns highlights like number of datapoints, totaltimeseries, active time series, last received time for a specified metric",
Request: nil,
RequestQuery: new(metricsexplorertypes.MetricNameQuery),
RequestContentType: "",
Response: new(metricsexplorertypes.MetricHighlightsResponse),
ResponseContentType: "application/json",
@@ -145,7 +147,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/metrics/{metric_name}/alerts", handler.New(
if err := router.Handle("/api/v2/metrics/alerts", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricAlerts),
handler.OpenAPIDef{
ID: "GetMetricAlerts",
@@ -153,6 +155,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
Summary: "Get metric alerts",
Description: "This endpoint returns associated alerts for a specified metric",
Request: nil,
RequestQuery: new(metricsexplorertypes.MetricNameQuery),
RequestContentType: "",
Response: new(metricsexplorertypes.MetricAlertsResponse),
ResponseContentType: "application/json",
@@ -164,7 +167,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/metrics/{metric_name}/dashboards", handler.New(
if err := router.Handle("/api/v2/metrics/dashboards", handler.New(
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricDashboards),
handler.OpenAPIDef{
ID: "GetMetricDashboards",
@@ -172,6 +175,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
Summary: "Get metric dashboards",
Description: "This endpoint returns associated dashboards for a specified metric",
Request: nil,
RequestQuery: new(metricsexplorertypes.MetricNameQuery),
RequestContentType: "",
Response: new(metricsexplorertypes.MetricDashboardsResponse),
ResponseContentType: "application/json",

View File

@@ -413,27 +413,64 @@ func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter,
// NOTE: this method is not specific to infra monitoring — it queries attributes_metadata generically.
// Consider moving to telemetryMetaStore when a second use case emerges.
//
// getEarliestMetricTime returns the earliest first_reported_unix_milli across the
// given metric names. It is used solely for the end-time-before-retention check.
// When none of the metrics have ever been reported, it returns 0.
func (m *module) getEarliestMetricTime(ctx context.Context, metricNames []string) (uint64, error) {
// getMetricsExistenceAndEarliestTime checks which of the given metric names have been
// reported. It returns a list of missing metrics (those not found or with zero count)
// and the earliest first-reported timestamp across all present metrics.
// When all metrics are missing, minFirstReportedUnixMilli is 0.
// TODO(nikhilmantri0902, srikanthccv): This method was designed this way because querier errors if any of the metrics
// in the querier list was never sent, the QueryRange call throws not found error. Modify this method, if QueryRange
// behaviour changes towards this.
func (m *module) getMetricsExistenceAndEarliestTime(ctx context.Context, metricNames []string) ([]string, uint64, error) {
if len(metricNames) == 0 {
return 0, nil
return nil, 0, nil
}
sb := sqlbuilder.NewSelectBuilder()
sb.Select("min(first_reported_unix_milli) AS min_first_reported")
sb.Select("metric_name", "count(*) AS cnt", "min(first_reported_unix_milli) AS min_first_reported")
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.AttributesMetadataTableName))
sb.Where(sb.In("metric_name", sqlbuilder.List(metricNames)))
sb.GroupBy("metric_name")
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
var minFirstReported uint64
if err := m.telemetryStore.ClickhouseDB().QueryRow(ctx, query, args...).Scan(&minFirstReported); err != nil {
return 0, err
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
type metricInfo struct {
count uint64
minFirstReported uint64
}
found := make(map[string]metricInfo, len(metricNames))
for rows.Next() {
var name string
var cnt, minFR uint64
if err := rows.Scan(&name, &cnt, &minFR); err != nil {
return nil, 0, err
}
found[name] = metricInfo{count: cnt, minFirstReported: minFR}
}
if err := rows.Err(); err != nil {
return nil, 0, err
}
return minFirstReported, nil
var missingMetrics []string
var globalMinFirstReported uint64
for _, name := range metricNames {
info, ok := found[name]
if !ok || info.count == 0 {
missingMetrics = append(missingMetrics, name)
continue
}
if globalMinFirstReported == 0 || info.minFirstReported < globalMinFirstReported {
globalMinFirstReported = info.minFirstReported
}
}
return missingMetrics, globalMinFirstReported, nil
}
// getMetadata fetches the latest values of additionalCols for each unique combination of groupBy keys,

View File

@@ -78,18 +78,26 @@ func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframon
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
}
// If req.End is before the earliest reported time for these metrics, return early
// with endTimeBeforeRetention=true.
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, hostsTableMetricNamesList)
// 1. Check which required metrics exist and get earliest retention time.
// If any required metric is missing, return early with the list of missing metrics.
// 2. If metrics exist but req.End is before the earliest reported time, return early with endTimeBeforeRetention=true.
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, hostsTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.HostRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.HostRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
// TOD(nikhilmantri0902): replace this separate ClickHouse query with a sub-query inside the main query builder query
// once QB supports sub-queries.
@@ -183,16 +191,23 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
}
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, podsTableMetricNamesList)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, podsTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.PodRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.PodRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getPodsTableMetadata(ctx, req)
if err != nil {
@@ -261,16 +276,23 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
}
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, nodesTableMetricNamesList)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, nodesTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.NodeRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.NodeRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getNodesTableMetadata(ctx, req)
if err != nil {
@@ -344,16 +366,23 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
}
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, namespacesTableMetricNamesList)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, namespacesTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.NamespaceRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.NamespaceRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getNamespacesTableMetadata(ctx, req)
if err != nil {
@@ -421,16 +450,23 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
}
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, clustersTableMetricNamesList)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, clustersTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.ClusterRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.ClusterRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getClustersTableMetadata(ctx, req)
if err != nil {
@@ -511,16 +547,23 @@ func (m *module) ListVolumes(ctx context.Context, orgID valuer.UUID, req *infram
}
req.Filter.Expression = mergeFilterExpressions(volumesBaseFilterExpr, req.Filter.Expression)
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, volumesTableMetricNamesList)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, volumesTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.VolumeRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.VolumeRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getVolumesTableMetadata(ctx, req)
if err != nil {
@@ -589,16 +632,23 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
}
req.Filter.Expression = mergeFilterExpressions(deploymentsBaseFilterExpr, req.Filter.Expression)
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, deploymentsTableMetricNamesList)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, deploymentsTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.DeploymentRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.DeploymentRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getDeploymentsTableMetadata(ctx, req)
if err != nil {
@@ -672,16 +722,23 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
}
req.Filter.Expression = mergeFilterExpressions(statefulSetsBaseFilterExpr, req.Filter.Expression)
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, statefulSetsTableMetricNamesList)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, statefulSetsTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.StatefulSetRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.StatefulSetRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getStatefulSetsTableMetadata(ctx, req)
if err != nil {
@@ -757,16 +814,23 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
}
req.Filter.Expression = mergeFilterExpressions(jobsBaseFilterExpr, req.Filter.Expression)
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, jobsTableMetricNamesList)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, jobsTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.JobRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.JobRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getJobsTableMetadata(ctx, req)
if err != nil {
@@ -842,16 +906,23 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
}
req.Filter.Expression = mergeFilterExpressions(daemonSetsBaseFilterExpr, req.Filter.Expression)
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, daemonSetsTableMetricNamesList)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, daemonSetsTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.DaemonSetRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.DaemonSetRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getDaemonSetsTableMetadata(ctx, req)
if err != nil {

View File

@@ -11,17 +11,8 @@ import (
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/metricsexplorertypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
func extractMetricName(req *http.Request) (string, error) {
metricName := mux.Vars(req)["metric_name"]
if metricName == "" {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "metric_name is required in URL path")
}
return metricName, nil
}
type handler struct {
module metricsexplorer.Module
}
@@ -116,23 +107,17 @@ func (h *handler) UpdateMetricMetadata(rw http.ResponseWriter, req *http.Request
return
}
// Extract metric_name from URL path
vars := mux.Vars(req)
metricName := vars["metric_name"]
if metricName == "" {
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "metric_name is required in URL path"))
return
}
var in metricsexplorertypes.UpdateMetricMetadataRequest
if err := binding.JSON.BindBody(req.Body, &in); err != nil {
render.Error(rw, err)
return
}
// Set metric name from URL path
in.MetricName = metricName
if in.MetricName == "" {
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName is required"))
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
err = h.module.UpdateMetricMetadata(req.Context(), orgID, &in)
@@ -151,11 +136,16 @@ func (h *handler) GetMetricMetadata(rw http.ResponseWriter, req *http.Request) {
return
}
metricName, err := extractMetricName(req)
if err != nil {
var in metricsexplorertypes.MetricNameQuery
if err := binding.Query.BindQuery(req.URL.Query(), &in); err != nil {
render.Error(rw, err)
return
}
if err := in.Validate(); err != nil {
render.Error(rw, err)
return
}
metricName := in.MetricName
orgID := valuer.MustNewUUID(claims.OrgID)
@@ -181,20 +171,24 @@ func (h *handler) GetMetricAlerts(rw http.ResponseWriter, req *http.Request) {
return
}
metricName, err := extractMetricName(req)
if err != nil {
var in metricsexplorertypes.MetricNameQuery
if err := binding.Query.BindQuery(req.URL.Query(), &in); err != nil {
render.Error(rw, err)
return
}
if err := in.Validate(); err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
if err := h.checkMetricExists(req.Context(), orgID, metricName); err != nil {
if err := h.checkMetricExists(req.Context(), orgID, in.MetricName); err != nil {
render.Error(rw, err)
return
}
out, err := h.module.GetMetricAlerts(req.Context(), orgID, metricName)
out, err := h.module.GetMetricAlerts(req.Context(), orgID, in.MetricName)
if err != nil {
render.Error(rw, err)
return
@@ -209,20 +203,24 @@ func (h *handler) GetMetricDashboards(rw http.ResponseWriter, req *http.Request)
return
}
metricName, err := extractMetricName(req)
if err != nil {
var in metricsexplorertypes.MetricNameQuery
if err := binding.Query.BindQuery(req.URL.Query(), &in); err != nil {
render.Error(rw, err)
return
}
if err := in.Validate(); err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
if err := h.checkMetricExists(req.Context(), orgID, metricName); err != nil {
if err := h.checkMetricExists(req.Context(), orgID, in.MetricName); err != nil {
render.Error(rw, err)
return
}
out, err := h.module.GetMetricDashboards(req.Context(), orgID, metricName)
out, err := h.module.GetMetricDashboards(req.Context(), orgID, in.MetricName)
if err != nil {
render.Error(rw, err)
return
@@ -237,20 +235,24 @@ func (h *handler) GetMetricHighlights(rw http.ResponseWriter, req *http.Request)
return
}
metricName, err := extractMetricName(req)
if err != nil {
var in metricsexplorertypes.MetricNameQuery
if err := binding.Query.BindQuery(req.URL.Query(), &in); err != nil {
render.Error(rw, err)
return
}
if err := in.Validate(); err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
if err := h.checkMetricExists(req.Context(), orgID, metricName); err != nil {
if err := h.checkMetricExists(req.Context(), orgID, in.MetricName); err != nil {
render.Error(rw, err)
return
}
highlights, err := h.module.GetMetricHighlights(req.Context(), orgID, metricName)
highlights, err := h.module.GetMetricHighlights(req.Context(), orgID, in.MetricName)
if err != nil {
render.Error(rw, err)
return
@@ -265,20 +267,12 @@ func (h *handler) GetMetricAttributes(rw http.ResponseWriter, req *http.Request)
return
}
metricName, err := extractMetricName(req)
if err != nil {
render.Error(rw, err)
return
}
var in metricsexplorertypes.MetricAttributesRequest
if err := binding.Query.BindQuery(req.URL.Query(), &in); err != nil {
render.Error(rw, err)
return
}
in.MetricName = metricName
if err := in.Validate(); err != nil {
render.Error(rw, err)
return
@@ -286,7 +280,7 @@ func (h *handler) GetMetricAttributes(rw http.ResponseWriter, req *http.Request)
orgID := valuer.MustNewUUID(claims.OrgID)
if err := h.checkMetricExists(req.Context(), orgID, metricName); err != nil {
if err := h.checkMetricExists(req.Context(), orgID, in.MetricName); err != nil {
render.Error(rw, err)
return
}

View File

@@ -80,8 +80,8 @@ type RoleWithTransactionGroups struct {
type PostableRole struct {
Name string `json:"name" required:"true"`
Description string `json:"description" required:"true"`
TransactionGroups TransactionGroups `json:"transactionGroups" required:"true" nullable:"false"`
Description string `json:"description" required:"false"`
TransactionGroups TransactionGroups `json:"transactionGroups" required:"false" nullable:"false"`
}
type UpdatableRole struct {
@@ -167,32 +167,40 @@ func (role *Role) ErrIfManaged() error {
}
func (role *PostableRole) UnmarshalJSON(data []byte) error {
type Alias PostableRole
var temp Alias
shadow := struct {
Name string `json:"name"`
Description string `json:"description"`
TransactionGroups *json.RawMessage `json:"transactionGroups"`
}{}
if err := json.Unmarshal(data, &temp); err != nil {
if err := json.Unmarshal(data, &shadow); err != nil {
return err
}
if temp.Name == "" {
if shadow.Name == "" {
return errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "name is missing from the request")
}
if match := roleNameRegex.MatchString(temp.Name); !match {
if match := roleNameRegex.MatchString(shadow.Name); !match {
return errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "name must contain only lowercase letters (a-z) and hyphens (-), and be at most 50 characters long.")
}
if strings.HasPrefix(temp.Name, managedRolePrefix) {
if strings.HasPrefix(shadow.Name, managedRolePrefix) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "role name cannot start with %q as it is reserved for SigNoz managed roles.", managedRolePrefix)
}
if temp.TransactionGroups == nil {
return errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "transactionGroups is required").WithAdditional("send an empty array to create a role with no transaction groups")
var transactionGroups TransactionGroups
if shadow.TransactionGroups != nil {
var err error
transactionGroups, err = NewTransactionGroups(*shadow.TransactionGroups)
if err != nil {
return err
}
}
role.Name = temp.Name
role.Description = temp.Description
role.TransactionGroups = temp.TransactionGroups
role.Name = shadow.Name
role.Description = shadow.Description
role.TransactionGroups = transactionGroups
return nil
}
@@ -206,9 +214,6 @@ func (role *UpdatableRole) UnmarshalJSON(data []byte) error {
return err
}
// A pointer distinguishes an omitted/null description from an explicit empty string: the field
// must be sent (update reconciles to exactly what is given), but an empty string is allowed so a
// caller can deliberately clear the description.
if shadow.Description == nil {
return errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "description is required").WithAdditional("send an empty string to clear the description")
}

View File

@@ -3,10 +3,22 @@ package authtypes
import (
"encoding/json"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type rawTransactionGroup struct {
Relation string `json:"relation"`
ObjectGroup struct {
Resource struct {
Type string `json:"type"`
Kind string `json:"kind"`
} `json:"resource"`
Selectors []string `json:"selectors"`
} `json:"objectGroup"`
}
type Transaction struct {
ID valuer.UUID `json:"-"`
Relation Relation `json:"relation" required:"true"`
@@ -39,16 +51,23 @@ func NewTransaction(relation Relation, object coretypes.Object) (*Transaction, e
return &Transaction{ID: valuer.GenerateUUID(), Relation: relation, Object: object}, nil
}
func NewTransactionGroup(relation Relation, objectGroup coretypes.ObjectGroup) (*TransactionGroup, error) {
if err := coretypes.ErrIfVerbNotValidForResource(relation.Verb, objectGroup.Resource); err != nil {
return nil, err
func NewTransactionGroups(data []byte) (TransactionGroups, error) {
var rawGroups []rawTransactionGroup
if err := json.Unmarshal(data, &rawGroups); err != nil {
return nil, errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "transactionGroups must be an array of {relation, objectGroup} objects")
}
if _, err := coretypes.NewObjectsFromObjectGroup(objectGroup); err != nil {
return nil, err
groups := make(TransactionGroups, 0, len(rawGroups))
for index, rawGroup := range rawGroups {
group, err := newTransactionGroup(rawGroup, index)
if err != nil {
return nil, err
}
groups = append(groups, group)
}
return &TransactionGroup{Relation: relation, ObjectGroup: objectGroup}, nil
return groups, nil
}
func NewGettableTransaction(results []*TransactionWithAuthorization) []*GettableTransaction {
@@ -88,26 +107,6 @@ func (transaction *Transaction) UnmarshalJSON(data []byte) error {
return nil
}
func (transactionGroup *TransactionGroup) UnmarshalJSON(data []byte) error {
var shadow = struct {
Relation Relation
ObjectGroup coretypes.ObjectGroup
}{}
err := json.Unmarshal(data, &shadow)
if err != nil {
return err
}
group, err := NewTransactionGroup(shadow.Relation, shadow.ObjectGroup)
if err != nil {
return err
}
*transactionGroup = *group
return nil
}
func (transaction *Transaction) TransactionKey() string {
return transaction.Relation.StringValue() + ":" + transaction.Object.Resource.Type.StringValue() + ":" + transaction.Object.Resource.Kind.String()
}
@@ -156,3 +155,39 @@ func (groups TransactionGroups) selectorSet() map[string]struct{} {
func (group *TransactionGroup) selectorKey(selector coretypes.Selector) string {
return group.Relation.StringValue() + "|" + group.ObjectGroup.Resource.String() + "|" + selector.String()
}
func newTransactionGroup(raw rawTransactionGroup, index int) (*TransactionGroup, error) {
verb, err := coretypes.NewVerb(raw.Relation)
if err != nil {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "transactionGroups[%d].relation: %s", index, err.Error())
}
resourceType, err := coretypes.NewType(raw.ObjectGroup.Resource.Type)
if err != nil {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "transactionGroups[%d].objectGroup.resource.type: %s", index, err.Error())
}
kind, err := coretypes.NewKind(raw.ObjectGroup.Resource.Kind)
if err != nil {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "transactionGroups[%d].objectGroup.resource.kind: %s", index, err.Error())
}
resourceRef := coretypes.ResourceRef{Type: resourceType, Kind: kind}
if err := coretypes.ErrIfVerbNotValidForResource(verb, resourceRef); err != nil {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "transactionGroups[%d]: %s", index, err.Error())
}
selectors := make([]coretypes.Selector, 0, len(raw.ObjectGroup.Selectors))
for selectorIndex, rawSelector := range raw.ObjectGroup.Selectors {
selector, err := resourceType.Selector(rawSelector)
if err != nil {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "transactionGroups[%d].objectGroup.selectors[%d]: %s", index, selectorIndex, err.Error())
}
selectors = append(selectors, selector)
}
return &TransactionGroup{
Relation: Relation{Verb: verb},
ObjectGroup: coretypes.ObjectGroup{Resource: resourceRef, Selectors: selectors},
}, nil
}

View File

@@ -39,6 +39,48 @@ func MustNewKind(str string) Kind {
return kind
}
func (name Kind) Enum() []any {
return []any{
KindAnonymous,
KindOrganization,
KindRole,
KindServiceAccount,
KindUser,
KindNotificationChannel,
KindRoutePolicy,
KindApdexSetting,
KindAuthDomain,
KindSession,
KindCloudIntegration,
KindCloudIntegrationService,
KindIntegration,
KindDashboard,
KindPublicDashboard,
KindIngestionKey,
KindIngestionLimit,
KindPipeline,
KindUserPreference,
KindOrgPreference,
KindQuickFilter,
KindTTLSetting,
KindRule,
KindPlannedMaintenance,
KindSavedView,
KindTraceFunnel,
KindFactorPassword,
KindFactorAPIKey,
KindLicense,
KindSubscription,
KindLogs,
KindTraces,
KindMetrics,
KindAuditLogs,
KindMeterMetrics,
KindLogsField,
KindTracesField,
}
}
func (name Kind) String() string {
return name.val
}

View File

@@ -12,6 +12,7 @@ type Clusters struct {
Type ResponseType `json:"type" required:"true"`
Records []ClusterRecord `json:"records" required:"true" nullable:"false"`
Total int `json:"total" required:"true"`
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
Warning *qbtypes.QueryWarnData `json:"warning,omitempty"`
}

View File

@@ -12,6 +12,7 @@ type DaemonSets struct {
Type ResponseType `json:"type" required:"true"`
Records []DaemonSetRecord `json:"records" required:"true" nullable:"false"`
Total int `json:"total" required:"true"`
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
Warning *qbtypes.QueryWarnData `json:"warning,omitempty"`
}

View File

@@ -12,6 +12,7 @@ type Deployments struct {
Type ResponseType `json:"type" required:"true"`
Records []DeploymentRecord `json:"records" required:"true" nullable:"false"`
Total int `json:"total" required:"true"`
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
Warning *qbtypes.QueryWarnData `json:"warning,omitempty"`
}

View File

@@ -12,6 +12,7 @@ type Hosts struct {
Type ResponseType `json:"type" required:"true"`
Records []HostRecord `json:"records" required:"true" nullable:"false"`
Total int `json:"total" required:"true"`
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
Warning *qbtypes.QueryWarnData `json:"warning,omitempty"`
}
@@ -29,6 +30,10 @@ type HostRecord struct {
Meta map[string]string `json:"meta" required:"true"`
}
type RequiredMetricsCheck struct {
MissingMetrics []string `json:"missingMetrics" required:"true"`
}
type PostableHosts struct {
Start int64 `json:"start" required:"true"`
End int64 `json:"end" required:"true"`

View File

@@ -12,6 +12,7 @@ type Jobs struct {
Type ResponseType `json:"type" required:"true"`
Records []JobRecord `json:"records" required:"true" nullable:"false"`
Total int `json:"total" required:"true"`
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
Warning *qbtypes.QueryWarnData `json:"warning,omitempty"`
}

View File

@@ -12,6 +12,7 @@ type Namespaces struct {
Type ResponseType `json:"type" required:"true"`
Records []NamespaceRecord `json:"records" required:"true" nullable:"false"`
Total int `json:"total" required:"true"`
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
Warning *qbtypes.QueryWarnData `json:"warning,omitempty"`
}

View File

@@ -12,6 +12,7 @@ type Nodes struct {
Type ResponseType `json:"type" required:"true"`
Records []NodeRecord `json:"records" required:"true" nullable:"false"`
Total int `json:"total" required:"true"`
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
Warning *qbtypes.QueryWarnData `json:"warning,omitempty"`
}

View File

@@ -12,6 +12,7 @@ type Pods struct {
Type ResponseType `json:"type" required:"true"`
Records []PodRecord `json:"records" required:"true" nullable:"false"`
Total int `json:"total" required:"true"`
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
Warning *qbtypes.QueryWarnData `json:"warning,omitempty"`
}

View File

@@ -12,6 +12,7 @@ type StatefulSets struct {
Type ResponseType `json:"type" required:"true"`
Records []StatefulSetRecord `json:"records" required:"true" nullable:"false"`
Total int `json:"total" required:"true"`
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
Warning *qbtypes.QueryWarnData `json:"warning,omitempty"`
}

View File

@@ -12,6 +12,7 @@ type Volumes struct {
Type ResponseType `json:"type" required:"true"`
Records []VolumeRecord `json:"records" required:"true" nullable:"false"`
Total int `json:"total" required:"true"`
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
Warning *qbtypes.QueryWarnData `json:"warning,omitempty"`
}

View File

@@ -260,11 +260,27 @@ type MetricHighlightsResponse struct {
ActiveTimeSeries uint64 `json:"activeTimeSeries" required:"true"`
}
// MetricNameQuery represents the query parameters for endpoints that take a metric name.
type MetricNameQuery struct {
MetricName string `query:"metricName" required:"true" description:"The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies)."`
}
// Validate ensures MetricNameQuery contains acceptable values.
func (q *MetricNameQuery) Validate() error {
if q == nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
}
if q.MetricName == "" {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName is required")
}
return nil
}
// MetricAttributesRequest represents the query parameters for the metric attributes endpoint.
type MetricAttributesRequest struct {
MetricName string `json:"-"`
Start *int64 `query:"start"`
End *int64 `query:"end"`
MetricName string `query:"metricName" required:"true" description:"The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies)."`
Start *int64 `query:"start" description:"Start of the time range as a Unix timestamp in milliseconds."`
End *int64 `query:"end" description:"End of the time range as a Unix timestamp in milliseconds."`
}
// Validate ensures MetricAttributesRequest contains acceptable values.
@@ -273,6 +289,10 @@ func (req *MetricAttributesRequest) Validate() error {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
}
if req.MetricName == "" {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName is required")
}
if req.Start != nil && req.End != nil {
if *req.Start >= *req.End {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "start (%d) must be less than end (%d)", *req.Start, *req.End)

View File

@@ -0,0 +1,3 @@
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"miss-p1-uid","k8s.pod.name":"miss-p1","k8s.daemonset.name":"miss-ds","k8s.node.name":"node-x","k8s.namespace.name":"ns-miss","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"miss-p1-uid","k8s.pod.name":"miss-p1","k8s.daemonset.name":"miss-ds","k8s.node.name":"node-x","k8s.namespace.name":"ns-miss","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"miss-p1-uid","k8s.pod.name":"miss-p1","k8s.daemonset.name":"miss-ds","k8s.node.name":"node-x","k8s.namespace.name":"ns-miss","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}

View File

@@ -0,0 +1,3 @@
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"miss-p1-uid","k8s.pod.name":"miss-p1","k8s.statefulset.name":"miss-ss","k8s.namespace.name":"ns-miss","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"miss-p1-uid","k8s.pod.name":"miss-p1","k8s.statefulset.name":"miss-ss","k8s.namespace.name":"ns-miss","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.pod.cpu.usage","labels":{"k8s.pod.uid":"miss-p1-uid","k8s.pod.name":"miss-p1","k8s.statefulset.name":"miss-ss","k8s.namespace.name":"ns-miss","k8s.cluster.name":"cluster-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":0.5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}

View File

@@ -1,12 +0,0 @@
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "fop-pvc", "k8s.pod.uid": "fop-pvc-uid", "k8s.pod.name": "pod-fop-pvc", "k8s.namespace.name": "ns-acc", "k8s.node.name": "node-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "ss-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 100000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "fop-pvc", "k8s.pod.uid": "fop-pvc-uid", "k8s.pod.name": "pod-fop-pvc", "k8s.namespace.name": "ns-acc", "k8s.node.name": "node-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "ss-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 100000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "fop-pvc", "k8s.pod.uid": "fop-pvc-uid", "k8s.pod.name": "pod-fop-pvc", "k8s.namespace.name": "ns-acc", "k8s.node.name": "node-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "ss-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 100000000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "fop-pvc", "k8s.pod.uid": "fop-pvc-uid", "k8s.pod.name": "pod-fop-pvc", "k8s.namespace.name": "ns-acc", "k8s.node.name": "node-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "ss-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "fop-pvc", "k8s.pod.uid": "fop-pvc-uid", "k8s.pod.name": "pod-fop-pvc", "k8s.namespace.name": "ns-acc", "k8s.node.name": "node-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "ss-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "fop-pvc", "k8s.pod.uid": "fop-pvc-uid", "k8s.pod.name": "pod-fop-pvc", "k8s.namespace.name": "ns-acc", "k8s.node.name": "node-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "ss-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "fop-pvc", "k8s.pod.uid": "fop-pvc-uid", "k8s.pod.name": "pod-fop-pvc", "k8s.namespace.name": "ns-acc", "k8s.node.name": "node-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "ss-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 800000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "fop-pvc", "k8s.pod.uid": "fop-pvc-uid", "k8s.pod.name": "pod-fop-pvc", "k8s.namespace.name": "ns-acc", "k8s.node.name": "node-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "ss-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 800000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "fop-pvc", "k8s.pod.uid": "fop-pvc-uid", "k8s.pod.name": "pod-fop-pvc", "k8s.namespace.name": "ns-acc", "k8s.node.name": "node-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "ss-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 800000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "fop-pvc", "k8s.pod.uid": "fop-pvc-uid", "k8s.pod.name": "pod-fop-pvc", "k8s.namespace.name": "ns-acc", "k8s.node.name": "node-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "ss-x"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 200000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "fop-pvc", "k8s.pod.uid": "fop-pvc-uid", "k8s.pod.name": "pod-fop-pvc", "k8s.namespace.name": "ns-acc", "k8s.node.name": "node-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "ss-x"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 200000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "fop-pvc", "k8s.pod.uid": "fop-pvc-uid", "k8s.pod.name": "pod-fop-pvc", "k8s.namespace.name": "ns-acc", "k8s.node.name": "node-x", "k8s.cluster.name": "cluster-x", "k8s.statefulset.name": "ss-x"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 200000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}

View File

@@ -0,0 +1,3 @@
{"metric_name":"k8s.volume.available","labels":{"k8s.persistentvolumeclaim.name":"miss-pvc","k8s.pod.uid":"pod-x-uid","k8s.pod.name":"pod-x","k8s.namespace.name":"ns-x","k8s.node.name":"node-x","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"ss-x"},"timestamp":"2025-01-10T10:00:00+00:00","value":1000000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.volume.available","labels":{"k8s.persistentvolumeclaim.name":"miss-pvc","k8s.pod.uid":"pod-x-uid","k8s.pod.name":"pod-x","k8s.namespace.name":"ns-x","k8s.node.name":"node-x","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"ss-x"},"timestamp":"2025-01-10T10:02:00+00:00","value":1000000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}
{"metric_name":"k8s.volume.available","labels":{"k8s.persistentvolumeclaim.name":"miss-pvc","k8s.pod.uid":"pod-x-uid","k8s.pod.name":"pod-x","k8s.namespace.name":"ns-x","k8s.node.name":"node-x","k8s.cluster.name":"cluster-x","k8s.statefulset.name":"ss-x"},"timestamp":"2025-01-10T10:04:00+00:00","value":1000000000.0,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false}

View File

@@ -11,7 +11,7 @@ from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.fs import get_testdata_file_path
from fixtures.metrics import Metrics
from fixtures.querier import compare_values, get_all_warnings
from fixtures.querier import compare_values
ENDPOINT = "/api/v2/infra_monitoring/hosts"
@@ -56,8 +56,7 @@ def test_hosts_accuracy(
# Shape/contract.
assert data["total"] == len(expected["records"])
assert len(data["records"]) == len(expected["records"])
# Full data present -> no warnings surfaced.
assert get_all_warnings(response.json()) == []
assert data["requiredMetricsCheck"]["missingMetrics"] == []
assert data["endTimeBeforeRetention"] is False
assert {r["hostName"] for r in data["records"]} == set(exp_by_host.keys())
@@ -80,81 +79,45 @@ def test_hosts_accuracy(
assert compare_values(record[field], exp[field], 1e-9), f"{record['hostName']}.{field}: got {record[field]}, expected {exp[field]}"
@pytest.mark.parametrize(
"case",
[
# Scenario 1: a required host metric was never ingested. Post-#11754 the
# querier drops it instead of hard-erroring, so the endpoint returns 200
# with the hosts that DO have data; the never-seen metric's column is the
# -1 sentinel and a "has never been received" warning is surfaced.
pytest.param(
{
"dataset": "hosts_missing_metrics.jsonl", # seeds only system.cpu.time
"body": {"filter": {"expression": "host.name = 'miss-h1'"}},
# singular form is "has", plural (>1 missing) is "have" -> match the common stem
"warn_substrings": ["never been received"],
"warn_names": [
"system.memory.usage",
"system.cpu.load_average.15m",
"system.filesystem.usage",
],
# cpu/wait derive from the present system.cpu.time; the rest come
# from never-seen metrics -> -1 sentinel.
"data_fields": ["cpu", "wait"],
"no_data_fields": ["memory", "load15", "diskUsage"],
},
id="metric_never_seen",
),
],
)
def test_hosts_warnings(
def test_hosts_missing_metrics(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
case: dict,
) -> None:
"""Data-availability gaps surface as non-blocking warnings (200 + data),
not hard errors. Covers never-seen metrics surfacing a "never been
received" warning while present metrics still return data."""
"""Seed only system.cpu.time; assert other 3 required metrics flagged missing."""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
Metrics.load_from_file(
get_testdata_file_path(f"inframonitoring/{case['dataset']}"),
get_testdata_file_path("inframonitoring/hosts_missing_metrics.jsonl"),
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
body: dict = {
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
}
body.update(case["body"])
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json=body,
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
assert response.status_code == HTTPStatus.OK
data = response.json()["data"]
warnings = get_all_warnings(response.json())
for substr in case["warn_substrings"]:
assert any(substr in w["message"] for w in warnings), f"{substr!r} not surfaced: {warnings!r}"
for name in case["warn_names"]:
assert any(name in w["message"] for w in warnings), f"{name!r} not surfaced: {warnings!r}"
assert len(data["records"]) >= 1, f"expected at least one record: {data!r}"
if case["data_fields"] or case["no_data_fields"]:
record = data["records"][0]
for field in case["data_fields"]:
assert record[field] != -1, f"expected {field} populated, got {record[field]}"
for field in case["no_data_fields"]:
assert record[field] == -1, f"expected {field} == -1 sentinel, got {record[field]}"
assert set(data["requiredMetricsCheck"]["missingMetrics"]) == {
"system.memory.usage",
"system.cpu.load_average.15m",
"system.filesystem.usage",
}
# Endpoint short-circuits when any required metric is missing:
# records is empty and total=0 regardless of which hosts have partial data.
# See pkg/modules/inframonitoring/implinframonitoring/module.go:84-89.
assert data["records"] == []
assert data["total"] == 0
@pytest.mark.parametrize(

View File

@@ -11,11 +11,26 @@ from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.fs import get_testdata_file_path
from fixtures.metrics import Metrics
from fixtures.querier import compare_values, get_all_warnings
from fixtures.querier import compare_values
from fixtures.time import parse_timestamp
ENDPOINT = "/api/v2/infra_monitoring/pods"
# Required metrics for the v2 pods endpoint
# (pkg/modules/inframonitoring/implinframonitoring/pods_constants.go:24-32).
REQUIRED_METRICS = {
"k8s.pod.cpu.usage",
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",
"k8s.pod.memory.working_set",
"k8s.pod.memory_request_utilization",
"k8s.pod.memory_limit_utilization",
"k8s.pod.phase",
}
# Numeric values emitted by the k8s.pod.phase metric (OTel kubeletstatsreceiver).
PHASE_NUM = {"pending": 1, "running": 2, "succeeded": 3, "failed": 4, "unknown": 5}
# Placeholder in JSONL labels that gets substituted with a runtime ISO string.
START_TIME_PLACEHOLDER = "__START_TIME__"
@@ -101,8 +116,7 @@ def test_pods_accuracy(
# Shape/contract.
assert data["total"] == len(expected["records"])
assert len(data["records"]) == len(expected["records"])
# Full data present -> no warnings surfaced.
assert get_all_warnings(response.json()) == []
assert data["requiredMetricsCheck"]["missingMetrics"] == []
assert data["endTimeBeforeRetention"] is False
assert {r["meta"]["k8s.pod.name"] for r in data["records"]} == set(exp_by_name.keys())
@@ -148,91 +162,42 @@ def test_pods_accuracy(
assert record["podAge"] == expected_age_ms, f"{pod_name}.podAge: got {record['podAge']}, expected {expected_age_ms}"
@pytest.mark.parametrize(
"case",
[
# Scenario 1: required metrics were never ingested. Post-#11754 the querier
# drops them (no hard error), so the endpoint returns 200 with the pod that
# DOES have data; never-seen columns are the -1 sentinel and a
# "have never been received" warning is surfaced. Pods has no formulas, so
# each missing metric maps straight to one -1 column.
pytest.param(
{
"dataset": "pods_missing_metrics.jsonl", # seeds only k8s.pod.cpu.usage
"body": {"filter": {"expression": "k8s.pod.name = 'miss-p1'"}},
"warn_substrings": ["never been received"],
"warn_names": [
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",
"k8s.pod.memory.working_set",
"k8s.pod.memory_request_utilization",
"k8s.pod.memory_limit_utilization",
],
# podCPU derives from the present k8s.pod.cpu.usage; the rest from
# never-seen metrics -> -1 sentinel.
"data_fields": ["podCPU"],
"no_data_fields": [
"podCPURequest",
"podCPULimit",
"podMemory",
"podMemoryRequest",
"podMemoryLimit",
],
},
id="metric_never_seen",
),
],
)
def test_pods_warnings(
def test_pods_missing_metrics(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
case: dict,
) -> None:
"""A never-ingested metric surfaces a non-blocking warning (200 + data), not a
hard error: the endpoint returns the entity that DOES have data and the
never-seen columns carry the -1 sentinel. (The generic never-seen
(metric, key)-pair-via-groupBy warning is entity-agnostic and is exercised
once, for hosts, in 01_hosts.py.)"""
"""Seed only k8s.pod.cpu.usage; assert other 6 required metrics flagged missing.
The endpoint short-circuits and returns empty records + total=0 when any
required metric is missing (module.go:192-197).
"""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
_load_pods_metrics(
f"inframonitoring/{case['dataset']}",
"inframonitoring/pods_missing_metrics.jsonl",
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
body: dict = {
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
}
body.update(case["body"])
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json=body,
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
warnings = get_all_warnings(response.json())
for substr in case["warn_substrings"]:
assert any(substr in w["message"] for w in warnings), f"{substr!r} not surfaced: {warnings!r}"
for name in case["warn_names"]:
assert any(name in w["message"] for w in warnings), f"{name!r} not surfaced: {warnings!r}"
assert len(data["records"]) >= 1, f"expected at least one record: {data!r}"
if case["data_fields"] or case["no_data_fields"]:
record = data["records"][0]
for field in case["data_fields"]:
assert record[field] != -1, f"expected {field} populated, got {record[field]}"
for field in case["no_data_fields"]:
assert record[field] == -1, f"expected {field} == -1 sentinel, got {record[field]}"
assert set(data["requiredMetricsCheck"]["missingMetrics"]) == (REQUIRED_METRICS - {"k8s.pod.cpu.usage"})
assert data["records"] == []
assert data["total"] == 0
@pytest.mark.parametrize(

View File

@@ -11,10 +11,24 @@ from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.fs import get_testdata_file_path
from fixtures.metrics import Metrics
from fixtures.querier import compare_values, get_all_warnings
from fixtures.querier import compare_values
ENDPOINT = "/api/v2/infra_monitoring/nodes"
# Required metrics for the v2 nodes endpoint
# (pkg/modules/inframonitoring/implinframonitoring/nodes_constants.go:22-29).
REQUIRED_METRICS = {
"k8s.node.cpu.usage",
"k8s.node.allocatable_cpu",
"k8s.node.memory.working_set",
"k8s.node.allocatable_memory",
"k8s.node.condition_ready",
"k8s.pod.phase",
}
# Numeric values emitted by k8s.node.condition_ready.
COND_NUM = {"ready": 1, "not_ready": 0}
def test_nodes_accuracy(
signoz: types.SigNoz,
@@ -57,8 +71,7 @@ def test_nodes_accuracy(
# Shape/contract.
assert data["total"] == len(expected["records"])
assert len(data["records"]) == len(expected["records"])
# Full data present -> no warnings surfaced.
assert get_all_warnings(response.json()) == []
assert data["requiredMetricsCheck"]["missingMetrics"] == []
assert data["endTimeBeforeRetention"] is False
assert {r["nodeName"] for r in data["records"]} == set(exp_by_name.keys())
@@ -94,83 +107,38 @@ def test_nodes_accuracy(
assert record["podCountsByPhase"] == exp["podCountsByPhase"]
@pytest.mark.parametrize(
"case",
[
# Scenario 1: required metrics were never ingested. Post-#11754 the querier
# drops them (no hard error), so the endpoint returns 200 with the node that
# DOES have data; never-seen columns are the -1 sentinel + a
# "have never been received" warning. Nodes has no formulas, so each missing
# metric maps straight to one -1 column.
pytest.param(
{
"dataset": "nodes_missing_metrics.jsonl", # seeds only k8s.node.cpu.usage
"body": {"filter": {"expression": "k8s.node.name = 'miss-n1'"}},
"warn_substrings": ["never been received"],
"warn_names": [
"k8s.node.allocatable_cpu",
"k8s.node.memory.working_set",
"k8s.node.allocatable_memory",
],
# nodeCPU derives from the present k8s.node.cpu.usage; the rest from
# never-seen metrics -> -1 sentinel.
"data_fields": ["nodeCPU"],
"no_data_fields": ["nodeCPUAllocatable", "nodeMemory", "nodeMemoryAllocatable"],
},
id="metric_never_seen",
),
],
)
def test_nodes_warnings(
def test_nodes_missing_metrics(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
case: dict,
) -> None:
"""A never-ingested metric surfaces a non-blocking warning (200 + data), not a
hard error: the endpoint returns the entity that DOES have data and the
never-seen columns carry the -1 sentinel. (The generic never-seen
(metric, key)-pair-via-groupBy warning is entity-agnostic and is exercised
once, for hosts, in 01_hosts.py.)"""
"""Seed only k8s.node.cpu.usage; assert other 5 required metrics flagged missing."""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
Metrics.load_from_file(
get_testdata_file_path(f"inframonitoring/{case['dataset']}"),
get_testdata_file_path("inframonitoring/nodes_missing_metrics.jsonl"),
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
body: dict = {
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
}
body.update(case["body"])
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json=body,
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
warnings = get_all_warnings(response.json())
for substr in case["warn_substrings"]:
assert any(substr in w["message"] for w in warnings), f"{substr!r} not surfaced: {warnings!r}"
for name in case["warn_names"]:
assert any(name in w["message"] for w in warnings), f"{name!r} not surfaced: {warnings!r}"
assert len(data["records"]) >= 1, f"expected at least one record: {data!r}"
if case["data_fields"] or case["no_data_fields"]:
record = data["records"][0]
for field in case["data_fields"]:
assert record[field] != -1, f"expected {field} populated, got {record[field]}"
for field in case["no_data_fields"]:
assert record[field] == -1, f"expected {field} == -1 sentinel, got {record[field]}"
assert set(data["requiredMetricsCheck"]["missingMetrics"]) == (REQUIRED_METRICS - {"k8s.node.cpu.usage"})
assert data["records"] == []
assert data["total"] == 0
@pytest.mark.parametrize(

View File

@@ -11,10 +11,18 @@ from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.fs import get_testdata_file_path
from fixtures.metrics import Metrics
from fixtures.querier import compare_values, get_all_warnings
from fixtures.querier import compare_values
ENDPOINT = "/api/v2/infra_monitoring/namespaces"
# Required metrics for the v2 namespaces endpoint
# (pkg/modules/inframonitoring/implinframonitoring/namespaces_constants.go:22-26).
REQUIRED_METRICS = {
"k8s.pod.cpu.usage",
"k8s.pod.memory.working_set",
"k8s.pod.phase",
}
def test_namespaces_accuracy(
signoz: types.SigNoz,
@@ -63,8 +71,7 @@ def test_namespaces_accuracy(
# Shape/contract.
assert data["total"] == len(expected["records"])
assert len(data["records"]) == len(expected["records"])
# Full data present -> no warnings surfaced.
assert get_all_warnings(response.json()) == []
assert data["requiredMetricsCheck"]["missingMetrics"] == []
assert data["endTimeBeforeRetention"] is False
assert {r["namespaceName"] for r in data["records"]} == set(exp_by_name.keys())
@@ -92,79 +99,38 @@ def test_namespaces_accuracy(
assert record["podCountsByPhase"] == exp["podCountsByPhase"]
@pytest.mark.parametrize(
"case",
[
# Scenario 1: a required metric was never ingested. Post-#11754 the querier
# drops it (no hard error), so the endpoint returns 200 with the namespace
# that DOES have data; the never-seen column is the -1 sentinel + a
# "have never been received" warning. Namespaces has no formulas (2 columns:
# namespaceCPU=A, namespaceMemory=D). Default groupBy is two keys
# [k8s.namespace.name, k8s.cluster.name]; the seed carries both so the
# page-groups IN-filter parses.
pytest.param(
{
"dataset": "namespaces_missing_metrics.jsonl", # seeds only k8s.pod.cpu.usage
"body": {"filter": {"expression": "k8s.namespace.name = 'miss-ns'"}},
"warn_substrings": ["never been received", "k8s.pod.memory.working_set"],
"warn_names": [],
"data_fields": ["namespaceCPU"],
"no_data_fields": ["namespaceMemory"],
},
id="metric_never_seen",
),
],
)
def test_namespaces_warnings(
def test_namespaces_missing_metrics(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
case: dict,
) -> None:
"""A never-ingested metric surfaces a non-blocking warning (200 + data), not a
hard error: the endpoint returns the entity that DOES have data and the
never-seen columns carry the -1 sentinel. (The generic never-seen
(metric, key)-pair-via-groupBy warning is entity-agnostic and is exercised
once, for hosts, in 01_hosts.py.)"""
"""Seed only k8s.pod.cpu.usage; assert other 2 required metrics flagged missing."""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
Metrics.load_from_file(
get_testdata_file_path(f"inframonitoring/{case['dataset']}"),
get_testdata_file_path("inframonitoring/namespaces_missing_metrics.jsonl"),
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
body: dict = {
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
}
body.update(case["body"])
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json=body,
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
warnings = get_all_warnings(response.json())
for substr in case["warn_substrings"]:
assert any(substr in w["message"] for w in warnings), f"{substr!r} not surfaced: {warnings!r}"
for name in case["warn_names"]:
assert any(name in w["message"] for w in warnings), f"{name!r} not surfaced: {warnings!r}"
assert len(data["records"]) >= 1, f"expected at least one record: {data!r}"
if case["data_fields"] or case["no_data_fields"]:
record = data["records"][0]
for field in case["data_fields"]:
assert record[field] != -1, f"expected {field} populated, got {record[field]}"
for field in case["no_data_fields"]:
assert record[field] == -1, f"expected {field} == -1 sentinel, got {record[field]}"
assert set(data["requiredMetricsCheck"]["missingMetrics"]) == (REQUIRED_METRICS - {"k8s.pod.cpu.usage"})
assert data["records"] == []
assert data["total"] == 0
@pytest.mark.parametrize(

View File

@@ -11,10 +11,21 @@ from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.fs import get_testdata_file_path
from fixtures.metrics import Metrics
from fixtures.querier import compare_values, get_all_warnings
from fixtures.querier import compare_values
ENDPOINT = "/api/v2/infra_monitoring/clusters"
# Required metrics for the v2 clusters endpoint
# (pkg/modules/inframonitoring/implinframonitoring/clusters_constants.go:23-30).
REQUIRED_METRICS = {
"k8s.node.cpu.usage",
"k8s.node.allocatable_cpu",
"k8s.node.memory.working_set",
"k8s.node.allocatable_memory",
"k8s.node.condition_ready",
"k8s.pod.phase",
}
def test_clusters_accuracy(
signoz: types.SigNoz,
@@ -63,8 +74,7 @@ def test_clusters_accuracy(
# Shape/contract.
assert data["total"] == len(expected["records"])
assert len(data["records"]) == len(expected["records"])
# Full data present -> no warnings surfaced.
assert get_all_warnings(response.json()) == []
assert data["requiredMetricsCheck"]["missingMetrics"] == []
assert data["endTimeBeforeRetention"] is False
assert {r["clusterName"] for r in data["records"]} == set(exp_by_name.keys())
@@ -103,82 +113,38 @@ def test_clusters_accuracy(
assert record["podCountsByPhase"] == exp["podCountsByPhase"]
@pytest.mark.parametrize(
"case",
[
# Scenario 1: a required metric was never ingested. Post-#11754 the querier
# drops it (no hard error), so the endpoint returns 200 with the cluster
# that DOES have data; never-seen columns are the -1 sentinel + a
# "have never been received" warning. Clusters has no formulas
# (clusterCPU=A, clusterCPUAllocatable=B, clusterMemory=C,
# clusterMemoryAllocatable=D).
pytest.param(
{
"dataset": "clusters_missing_metrics.jsonl", # seeds only k8s.node.cpu.usage
"body": {"filter": {"expression": "k8s.cluster.name = 'miss-cluster'"}},
"warn_substrings": ["never been received"],
"warn_names": [
"k8s.node.allocatable_cpu",
"k8s.node.memory.working_set",
"k8s.node.allocatable_memory",
],
"data_fields": ["clusterCPU"],
"no_data_fields": ["clusterCPUAllocatable", "clusterMemory", "clusterMemoryAllocatable"],
},
id="metric_never_seen",
),
],
)
def test_clusters_warnings(
def test_clusters_missing_metrics(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
case: dict,
) -> None:
"""A never-ingested metric surfaces a non-blocking warning (200 + data), not a
hard error: the endpoint returns the entity that DOES have data and the
never-seen columns carry the -1 sentinel. (The generic never-seen
(metric, key)-pair-via-groupBy warning is entity-agnostic and is exercised
once, for hosts, in 01_hosts.py.)"""
"""Seed only k8s.node.cpu.usage; assert other 5 required metrics flagged missing."""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
Metrics.load_from_file(
get_testdata_file_path(f"inframonitoring/{case['dataset']}"),
get_testdata_file_path("inframonitoring/clusters_missing_metrics.jsonl"),
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
body: dict = {
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
}
body.update(case["body"])
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json=body,
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
warnings = get_all_warnings(response.json())
for substr in case["warn_substrings"]:
assert any(substr in w["message"] for w in warnings), f"{substr!r} not surfaced: {warnings!r}"
for name in case["warn_names"]:
assert any(name in w["message"] for w in warnings), f"{name!r} not surfaced: {warnings!r}"
assert len(data["records"]) >= 1, f"expected at least one record: {data!r}"
if case["data_fields"] or case["no_data_fields"]:
record = data["records"][0]
for field in case["data_fields"]:
assert record[field] != -1, f"expected {field} populated, got {record[field]}"
for field in case["no_data_fields"]:
assert record[field] == -1, f"expected {field} == -1 sentinel, got {record[field]}"
assert set(data["requiredMetricsCheck"]["missingMetrics"]) == (REQUIRED_METRICS - {"k8s.node.cpu.usage"})
assert data["records"] == []
assert data["total"] == 0
@pytest.mark.parametrize(

View File

@@ -11,10 +11,20 @@ from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.fs import get_testdata_file_path
from fixtures.metrics import Metrics
from fixtures.querier import compare_values, get_all_warnings
from fixtures.querier import compare_values
ENDPOINT = "/api/v2/infra_monitoring/pvcs"
# Required metrics for the v2 volumes endpoint
# (pkg/modules/inframonitoring/implinframonitoring/volumes_constants.go:20-27).
REQUIRED_METRICS = {
"k8s.volume.available",
"k8s.volume.capacity",
"k8s.volume.inodes",
"k8s.volume.inodes.free",
"k8s.volume.inodes.used",
}
def test_volumes_accuracy(
signoz: types.SigNoz,
@@ -61,8 +71,7 @@ def test_volumes_accuracy(
# Shape/contract.
assert data["total"] == len(expected["records"])
assert len(data["records"]) == len(expected["records"])
# Full data present -> no warnings surfaced.
assert get_all_warnings(response.json()) == []
assert data["requiredMetricsCheck"]["missingMetrics"] == []
assert data["endTimeBeforeRetention"] is False
assert {r["persistentVolumeClaimName"] for r in data["records"]} == set(exp_by_name.keys())
@@ -108,81 +117,38 @@ def test_volumes_accuracy(
assert compare_values(record[field], exp[field], 1e-6), f"{record['persistentVolumeClaimName']}.{field}: got {record[field]}, expected {exp[field]}"
@pytest.mark.parametrize(
"case",
[
# Scenario 1: a metric was never ingested. Post-#11754 the querier drops it
# (no hard error), so the endpoint returns 200 with whatever flowed; the
# never-seen column is the -1 sentinel + a "have never been received"
# warning. Here we omit the FORMULA OPERAND k8s.volume.available
# (volumeUsage = capacity - available): since A uses TimeAggregationAvg it is
# NOT zero-defaultable, so the formula drops the group -> volumeUsage == -1
# (it does NOT fall back to capacity). capacity + inodes stay real.
pytest.param(
{
"dataset": "volumes_formula_operand_missing.jsonl",
"body": {"filter": {"expression": "k8s.persistentvolumeclaim.name = 'fop-pvc'"}},
"warn_substrings": ["never been received", "k8s.volume.available"],
"warn_names": [],
"data_fields": ["volumeCapacity", "volumeInodes", "volumeInodesFree", "volumeInodesUsed"],
"no_data_fields": ["volumeAvailable", "volumeUsage"],
},
id="metric_never_seen",
),
],
)
def test_volumes_warnings(
def test_volumes_missing_metrics(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
case: dict,
) -> None:
"""A never-ingested metric surfaces a non-blocking warning (200 + data), not a
hard error. Here the never-seen metric is the FORMULA OPERAND
k8s.volume.available (volumeUsage = capacity - available): since A uses
TimeAggregationAvg it is NOT zero-defaultable, so the formula drops the group
-> volumeUsage == -1 (it does NOT fall back to capacity); capacity + inodes
stay real. (The generic never-seen (metric, key)-pair-via-groupBy warning is
entity-agnostic and is exercised once, for hosts, in 01_hosts.py.)"""
"""Seed only k8s.volume.available; other 4 required metrics flagged missing."""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
Metrics.load_from_file(
get_testdata_file_path(f"inframonitoring/{case['dataset']}"),
get_testdata_file_path("inframonitoring/volumes_missing_metrics.jsonl"),
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
body: dict = {
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
}
body.update(case["body"])
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json=body,
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
warnings = get_all_warnings(response.json())
for substr in case["warn_substrings"]:
assert any(substr in w["message"] for w in warnings), f"{substr!r} not surfaced: {warnings!r}"
for name in case["warn_names"]:
assert any(name in w["message"] for w in warnings), f"{name!r} not surfaced: {warnings!r}"
assert len(data["records"]) >= 1, f"expected at least one record: {data!r}"
if case["data_fields"] or case["no_data_fields"]:
record = data["records"][0]
for field in case["data_fields"]:
assert record[field] != -1, f"expected {field} populated, got {record[field]}"
for field in case["no_data_fields"]:
assert record[field] == -1, f"expected {field} == -1 sentinel, got {record[field]}"
assert set(data["requiredMetricsCheck"]["missingMetrics"]) == (REQUIRED_METRICS - {"k8s.volume.available"})
assert data["records"] == []
assert data["total"] == 0
@pytest.mark.parametrize(

View File

@@ -11,10 +11,24 @@ from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.fs import get_testdata_file_path
from fixtures.metrics import Metrics
from fixtures.querier import compare_values, get_all_warnings
from fixtures.querier import compare_values
ENDPOINT = "/api/v2/infra_monitoring/deployments"
# Required metrics for the v2 deployments endpoint
# (pkg/modules/inframonitoring/implinframonitoring/deployments_constants.go:24-34).
REQUIRED_METRICS = {
"k8s.pod.phase",
"k8s.pod.cpu.usage",
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",
"k8s.pod.memory.working_set",
"k8s.pod.memory_request_utilization",
"k8s.pod.memory_limit_utilization",
"k8s.deployment.desired",
"k8s.deployment.available",
}
def test_deployments_accuracy(
signoz: types.SigNoz,
@@ -61,8 +75,7 @@ def test_deployments_accuracy(
# Shape/contract.
assert data["total"] == len(expected["records"])
assert len(data["records"]) == len(expected["records"])
# Full data present -> no warnings surfaced.
assert get_all_warnings(response.json()) == []
assert data["requiredMetricsCheck"]["missingMetrics"] == []
assert data["endTimeBeforeRetention"] is False
assert {r["deploymentName"] for r in data["records"]} == set(exp_by_name.keys())
@@ -110,89 +123,38 @@ def test_deployments_accuracy(
assert record["podCountsByPhase"] == exp["podCountsByPhase"]
@pytest.mark.parametrize(
"case",
[
# Scenario 1: required metrics were never ingested. Post-#11754 the querier
# drops them (no hard error), so the endpoint returns 200 with the deployment
# that DOES have data; never-seen columns are the -1 sentinel + a
# "have never been received" warning. No formulas; pod- and deployment-level
# metrics each map to one column.
pytest.param(
{
"dataset": "deployments_missing_metrics.jsonl", # seeds only k8s.pod.cpu.usage
"body": {"filter": {"expression": "k8s.deployment.name = 'miss-dep'"}},
"warn_substrings": ["never been received"],
"warn_names": [
"k8s.pod.memory.working_set",
"k8s.deployment.desired",
"k8s.deployment.available",
],
"data_fields": ["deploymentCPU"],
"no_data_fields": [
"deploymentCPURequest",
"deploymentCPULimit",
"deploymentMemory",
"deploymentMemoryRequest",
"deploymentMemoryLimit",
"desiredPods",
"availablePods",
],
},
id="metric_never_seen",
),
],
)
def test_deployments_warnings(
def test_deployments_missing_metrics(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
case: dict,
) -> None:
"""A never-ingested metric surfaces a non-blocking warning (200 + data), not a
hard error: the endpoint returns the entity that DOES have data and the
never-seen columns carry the -1 sentinel. (The generic never-seen
(metric, key)-pair-via-groupBy warning is entity-agnostic and is exercised
once, for hosts, in 01_hosts.py.)"""
"""Seed only k8s.pod.cpu.usage; assert other 8 required metrics flagged missing."""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
Metrics.load_from_file(
get_testdata_file_path(f"inframonitoring/{case['dataset']}"),
get_testdata_file_path("inframonitoring/deployments_missing_metrics.jsonl"),
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
body: dict = {
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
}
body.update(case["body"])
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json=body,
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
warnings = get_all_warnings(response.json())
for substr in case["warn_substrings"]:
assert any(substr in w["message"] for w in warnings), f"{substr!r} not surfaced: {warnings!r}"
for name in case["warn_names"]:
assert any(name in w["message"] for w in warnings), f"{name!r} not surfaced: {warnings!r}"
assert len(data["records"]) >= 1, f"expected at least one record: {data!r}"
if case["data_fields"] or case["no_data_fields"]:
record = data["records"][0]
for field in case["data_fields"]:
assert record[field] != -1, f"expected {field} populated, got {record[field]}"
for field in case["no_data_fields"]:
assert record[field] == -1, f"expected {field} == -1 sentinel, got {record[field]}"
assert set(data["requiredMetricsCheck"]["missingMetrics"]) == (REQUIRED_METRICS - {"k8s.pod.cpu.usage"})
assert data["records"] == []
assert data["total"] == 0
@pytest.mark.parametrize(

View File

@@ -11,10 +11,24 @@ from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.fs import get_testdata_file_path
from fixtures.metrics import Metrics
from fixtures.querier import compare_values, get_all_warnings
from fixtures.querier import compare_values
ENDPOINT = "/api/v2/infra_monitoring/statefulsets"
# Required metrics for the v2 statefulsets endpoint
# (pkg/modules/inframonitoring/implinframonitoring/statefulsets_constants.go:24-34).
REQUIRED_METRICS = {
"k8s.pod.phase",
"k8s.pod.cpu.usage",
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",
"k8s.pod.memory.working_set",
"k8s.pod.memory_request_utilization",
"k8s.pod.memory_limit_utilization",
"k8s.statefulset.desired_pods",
"k8s.statefulset.current_pods",
}
def test_statefulsets_accuracy(
signoz: types.SigNoz,
@@ -61,8 +75,7 @@ def test_statefulsets_accuracy(
# Shape/contract.
assert data["total"] == len(expected["records"])
assert len(data["records"]) == len(expected["records"])
# Full data present -> no warnings surfaced.
assert get_all_warnings(response.json()) == []
assert data["requiredMetricsCheck"]["missingMetrics"] == []
assert data["endTimeBeforeRetention"] is False
assert {r["statefulSetName"] for r in data["records"]} == set(exp_by_name.keys())
@@ -110,6 +123,40 @@ def test_statefulsets_accuracy(
assert record["podCountsByPhase"] == exp["podCountsByPhase"]
def test_statefulsets_missing_metrics(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
) -> None:
"""Seed only k8s.pod.cpu.usage; assert other 8 required metrics flagged missing."""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
Metrics.load_from_file(
get_testdata_file_path("inframonitoring/statefulsets_missing_metrics.jsonl"),
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
assert set(data["requiredMetricsCheck"]["missingMetrics"]) == (REQUIRED_METRICS - {"k8s.pod.cpu.usage"})
assert data["records"] == []
assert data["total"] == 0
@pytest.mark.parametrize(
"expression,expected",
[

View File

@@ -11,10 +11,26 @@ from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.fs import get_testdata_file_path
from fixtures.metrics import Metrics
from fixtures.querier import compare_values, get_all_warnings
from fixtures.querier import compare_values
ENDPOINT = "/api/v2/infra_monitoring/jobs"
# Required metrics for the v2 jobs endpoint
# (pkg/modules/inframonitoring/implinframonitoring/jobs_constants.go:24-36).
REQUIRED_METRICS = {
"k8s.pod.phase",
"k8s.pod.cpu.usage",
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",
"k8s.pod.memory.working_set",
"k8s.pod.memory_request_utilization",
"k8s.pod.memory_limit_utilization",
"k8s.job.active_pods",
"k8s.job.failed_pods",
"k8s.job.successful_pods",
"k8s.job.desired_successful_pods",
}
def test_jobs_accuracy(
signoz: types.SigNoz,
@@ -62,8 +78,7 @@ def test_jobs_accuracy(
# Shape/contract.
assert data["total"] == len(expected["records"])
assert len(data["records"]) == len(expected["records"])
# Full data present -> no warnings surfaced.
assert get_all_warnings(response.json()) == []
assert data["requiredMetricsCheck"]["missingMetrics"] == []
assert data["endTimeBeforeRetention"] is False
assert {r["jobName"] for r in data["records"]} == set(exp_by_name.keys())
@@ -113,91 +128,38 @@ def test_jobs_accuracy(
assert record["podCountsByPhase"] == exp["podCountsByPhase"]
@pytest.mark.parametrize(
"case",
[
# Scenario 1: required metrics were never ingested. Post-#11754 the querier
# drops them (no hard error), so the endpoint returns 200 with the job that
# DOES have data; never-seen columns are the -1 sentinel + a
# "have never been received" warning. No formulas; pod- and job-level
# metrics each map to one column.
pytest.param(
{
"dataset": "jobs_missing_metrics.jsonl", # seeds only k8s.pod.cpu.usage
"body": {"filter": {"expression": "k8s.job.name = 'miss-job'"}},
"warn_substrings": ["never been received"],
"warn_names": [
"k8s.pod.memory.working_set",
"k8s.job.desired_successful_pods",
"k8s.job.successful_pods",
],
"data_fields": ["jobCPU"],
"no_data_fields": [
"jobCPURequest",
"jobCPULimit",
"jobMemory",
"jobMemoryRequest",
"jobMemoryLimit",
"desiredSuccessfulPods",
"activePods",
"failedPods",
"successfulPods",
],
},
id="metric_never_seen",
),
],
)
def test_jobs_warnings(
def test_jobs_missing_metrics(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
case: dict,
) -> None:
"""A never-ingested metric surfaces a non-blocking warning (200 + data), not a
hard error: the endpoint returns the entity that DOES have data and the
never-seen columns carry the -1 sentinel. (The generic never-seen
(metric, key)-pair-via-groupBy warning is entity-agnostic and is exercised
once, for hosts, in 01_hosts.py.)"""
"""Seed only k8s.pod.cpu.usage; assert other 10 required metrics flagged missing."""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
Metrics.load_from_file(
get_testdata_file_path(f"inframonitoring/{case['dataset']}"),
get_testdata_file_path("inframonitoring/jobs_missing_metrics.jsonl"),
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
body: dict = {
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
}
body.update(case["body"])
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json=body,
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
warnings = get_all_warnings(response.json())
for substr in case["warn_substrings"]:
assert any(substr in w["message"] for w in warnings), f"{substr!r} not surfaced: {warnings!r}"
for name in case["warn_names"]:
assert any(name in w["message"] for w in warnings), f"{name!r} not surfaced: {warnings!r}"
assert len(data["records"]) >= 1, f"expected at least one record: {data!r}"
if case["data_fields"] or case["no_data_fields"]:
record = data["records"][0]
for field in case["data_fields"]:
assert record[field] != -1, f"expected {field} populated, got {record[field]}"
for field in case["no_data_fields"]:
assert record[field] == -1, f"expected {field} == -1 sentinel, got {record[field]}"
assert set(data["requiredMetricsCheck"]["missingMetrics"]) == (REQUIRED_METRICS - {"k8s.pod.cpu.usage"})
assert data["records"] == []
assert data["total"] == 0
@pytest.mark.parametrize(

View File

@@ -11,10 +11,24 @@ from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.fs import get_testdata_file_path
from fixtures.metrics import Metrics
from fixtures.querier import compare_values, get_all_warnings
from fixtures.querier import compare_values
ENDPOINT = "/api/v2/infra_monitoring/daemonsets"
# Required metrics for the v2 daemonsets endpoint
# (pkg/modules/inframonitoring/implinframonitoring/daemonsets_constants.go:24-34).
REQUIRED_METRICS = {
"k8s.pod.phase",
"k8s.pod.cpu.usage",
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",
"k8s.pod.memory.working_set",
"k8s.pod.memory_request_utilization",
"k8s.pod.memory_limit_utilization",
"k8s.daemonset.desired_scheduled_nodes",
"k8s.daemonset.current_scheduled_nodes",
}
def test_daemonsets_accuracy(
signoz: types.SigNoz,
@@ -61,8 +75,7 @@ def test_daemonsets_accuracy(
# Shape/contract.
assert data["total"] == len(expected["records"])
assert len(data["records"]) == len(expected["records"])
# Full data present -> no warnings surfaced.
assert get_all_warnings(response.json()) == []
assert data["requiredMetricsCheck"]["missingMetrics"] == []
assert data["endTimeBeforeRetention"] is False
assert {r["daemonSetName"] for r in data["records"]} == set(exp_by_name.keys())
@@ -110,6 +123,40 @@ def test_daemonsets_accuracy(
assert record["podCountsByPhase"] == exp["podCountsByPhase"]
def test_daemonsets_missing_metrics(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token,
insert_metrics,
) -> None:
"""Seed only k8s.pod.cpu.usage; assert other 8 required metrics flagged missing."""
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
Metrics.load_from_file(
get_testdata_file_path("inframonitoring/daemonsets_missing_metrics.jsonl"),
base_time=now - timedelta(minutes=4),
)
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.post(
signoz.self.host_configs["8080"].get(ENDPOINT),
headers={"authorization": f"Bearer {token}"},
json={
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
"end": int(now.timestamp() * 1000),
"limit": 50,
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()["data"]
assert set(data["requiredMetricsCheck"]["missingMetrics"]) == (REQUIRED_METRICS - {"k8s.pod.cpu.usage"})
assert data["records"] == []
assert data["total"] == 0
@pytest.mark.parametrize(
"expression,expected",
[