mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-25 17:40:32 +01:00
Compare commits
4 Commits
main
...
metric-red
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bc966adec | ||
|
|
112ff4ec78 | ||
|
|
09e9466dab | ||
|
|
7da9214e8c |
@@ -29,6 +29,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule/implmetricreductionrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
@@ -119,6 +121,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(_ sqlstore.SQLStore, _ dashboard.Module, _ global.Global, _ zeus.Zeus, _ gateway.Gateway, _ licensing.Licensing, _ serviceaccount.Module, _ cloudintegration.Config) (cloudintegration.Module, error) {
|
||||
return implcloudintegration.NewModule(), nil
|
||||
},
|
||||
func(_ sqlstore.SQLStore, _ telemetrystore.TelemetryStore, _ dashboard.Module, _ queryparser.QueryParser, _ licensing.Licensing, _ flagger.Flagger, _ telemetrytypes.MetadataStore, _ factory.ProviderSettings, _ int) metricreductionrule.Module {
|
||||
return implmetricreductionrule.NewModule()
|
||||
},
|
||||
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
|
||||
return factory.MustNewNamedMap(signozruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, nil, nil))
|
||||
},
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/modules/cloudintegration/implcloudintegration"
|
||||
"github.com/SigNoz/signoz/ee/modules/cloudintegration/implcloudintegration/implcloudprovider"
|
||||
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
|
||||
eeimplmetricreductionrule "github.com/SigNoz/signoz/ee/modules/metricreductionrule/implmetricreductionrule"
|
||||
eequerier "github.com/SigNoz/signoz/ee/querier"
|
||||
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
|
||||
eerules "github.com/SigNoz/signoz/ee/query-service/rules"
|
||||
@@ -46,6 +47,7 @@ import (
|
||||
pkgcloudintegration "github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
@@ -182,6 +184,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
|
||||
return implcloudintegration.NewModule(pkgcloudintegration.NewStore(sqlStore), dashboardModule, global, zeus, gateway, licensing, serviceAccount, cloudProvidersMap, config)
|
||||
},
|
||||
func(sqlStore sqlstore.SQLStore, ts telemetrystore.TelemetryStore, dashboardModule dashboard.Module, queryParser queryparser.QueryParser, lic licensing.Licensing, flgr pkgflagger.Flagger, ms telemetrytypes.MetadataStore, ps factory.ProviderSettings, threads int) metricreductionrule.Module {
|
||||
return eeimplmetricreductionrule.NewModule(sqlStore, ts, dashboardModule, queryParser, lic, flgr, ms, ps, threads)
|
||||
},
|
||||
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
|
||||
return factory.MustNewNamedMap(signozruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, eerules.PrepareTaskFunc, eerules.TestNotification))
|
||||
},
|
||||
|
||||
@@ -1,76 +1,48 @@
|
||||
# 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.
|
||||
# Migrating from the install script to Foundry
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 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].
|
||||
> The install script is now deprecated and will no longer receive updates.
|
||||
|
||||
## How it works
|
||||
This guide walks you through migrating an existing SigNoz deployment running via
|
||||
Docker Compose to [Foundry](https://signoz.io/docs/install/docker/).
|
||||
|
||||
Foundry splits a deployment into two commands:
|
||||
> [!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.
|
||||
|
||||
- `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.
|
||||
## Overview
|
||||
To stay up to date on new installation platforms and patterns, please refer to [Foundry](https://github.com/SigNoz/foundry).
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
## Prerequisites
|
||||
- [ ] Install Foundry - `curl -fsSL https://signoz.io/foundry.sh | bash`
|
||||
|
||||
- An existing SigNoz deployment from `install.sh` or `deploy/` (Compose or
|
||||
Swarm).
|
||||
- `foundryctl` (installed in step 1).
|
||||
## 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.
|
||||
|
||||
## Migrate
|
||||
1. Make a note of the volume names used by your existing deployment for the following components:
|
||||
- ClickHouse
|
||||
- SigNoz
|
||||
- ZooKeeper
|
||||
|
||||
### 1. Install Foundry
|
||||
> If you used the docker compose file we provided, the volumes will be `signoz-clickhouse`, `signoz-sqlite`, and `signoz-zookeeper-1`.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://signoz.io/foundry.sh | bash
|
||||
```
|
||||
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`.
|
||||
|
||||
### 2. Keep your rollback path
|
||||
Foundry has a [Docker Compose example](https://github.com/SigNoz/foundry/tree/main/docs/examples/docker/compose). Please use it as a reference.
|
||||
|
||||
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.
|
||||
> [!WARNING]
|
||||
> If your deployment had more than 1 shard or replica, you will need to adjust your manifest volumes accordingly.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 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>
|
||||
> 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.
|
||||
|
||||
```yaml
|
||||
# casting.yaml
|
||||
apiVersion: v1alpha1
|
||||
kind: Installation
|
||||
metadata:
|
||||
@@ -89,8 +61,8 @@ spec:
|
||||
data:
|
||||
config-0-0.yaml: |
|
||||
macros:
|
||||
replica: "example01-01-1" # replace with your replica macro
|
||||
shard: "01" # replace with your shard macro
|
||||
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)
|
||||
patches:
|
||||
- target: "deployment/compose.yaml"
|
||||
operations:
|
||||
@@ -108,163 +80,50 @@ spec:
|
||||
value: root
|
||||
```
|
||||
|
||||
</details>
|
||||
> [!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>
|
||||
<summary><b>Docker Swarm</b> casting.yaml</summary>
|
||||
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.
|
||||
|
||||
```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
|
||||
```
|
||||
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.
|
||||
|
||||
</details>
|
||||
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.
|
||||
|
||||
> [!NOTE]
|
||||
> 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.
|
||||
> This will generate downtime so please plan accordingly.
|
||||
|
||||
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].
|
||||
5. Validate the SigNoz containers are down with `docker ps -a`. Multiple containers cannot bind the same volume.
|
||||
|
||||
### 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
|
||||
```
|
||||
6. Run `foundryctl cast -f casting.yaml`. This will recreate the containers based on the spec. This process will download new container images.
|
||||
|
||||
> [!NOTE]
|
||||
> This causes downtime, so plan accordingly.
|
||||
> When `cast` is run, the migration container will execute its migrations.
|
||||
|
||||
Confirm nothing is still bound to the volumes before continuing:
|
||||
## 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.
|
||||
|
||||
```bash
|
||||
docker ps -a
|
||||
```
|
||||
## 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:
|
||||
|
||||
### 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.
|
||||
- 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.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the migration runs into trouble, reach out on [Slack][slack] or open a
|
||||
[Foundry issue][foundry-issues].
|
||||
- Please reach out to our community on [Slack](https://signoz.io/slack).
|
||||
|
||||
## References
|
||||
|
||||
- [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/
|
||||
[slack]: https://signoz.io/slack
|
||||
[foundry-issues]: https://github.com/SigNoz/foundry/issues
|
||||
[signoz-docs]: https://signoz.io/docs
|
||||
- [SigNoz Docker installation docs](https://signoz.io/docs/install/docker/)
|
||||
- [SigNoz documentation](https://signoz.io/docs)
|
||||
- [Foundry](https://github.com/SigNoz/foundry)
|
||||
|
||||
1108
docs/api/openapi.yml
1108
docs/api/openapi.yml
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,543 @@
|
||||
package implmetricreductionrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
sqlbuilder "github.com/huandu/go-sqlbuilder"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
|
||||
)
|
||||
|
||||
var (
|
||||
reductionRulesTable = telemetrymetrics.DBName + "." + telemetrymetrics.ReductionRulesTableName
|
||||
metadataTable = telemetrymetrics.DBName + "." + telemetrymetrics.AttributesMetadataTableName
|
||||
bufferSeriesTable = telemetrymetrics.DBName + "." + telemetrymetrics.TimeseriesV4BufferTableName
|
||||
)
|
||||
|
||||
const (
|
||||
timeSeriesBucketMilli = int64(time.Hour / time.Millisecond)
|
||||
sampleBucketMilli = int64(60 * time.Second / time.Millisecond)
|
||||
)
|
||||
|
||||
type volumeRow struct {
|
||||
MetricName string
|
||||
Ingested uint64
|
||||
Reduced uint64
|
||||
}
|
||||
|
||||
type volumePoint struct {
|
||||
TimestampMs int64
|
||||
Ingested uint64
|
||||
Reduced uint64
|
||||
}
|
||||
|
||||
type clickhouse struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
threads int
|
||||
}
|
||||
|
||||
func newClickhouse(telemetryStore telemetrystore.TelemetryStore, threads int) *clickhouse {
|
||||
return &clickhouse{telemetryStore: telemetryStore, threads: threads}
|
||||
}
|
||||
|
||||
func (c *clickhouse) withThreads(ctx context.Context) context.Context {
|
||||
return ctxtypes.SetClickhouseMaxThreads(ctx, c.threads)
|
||||
}
|
||||
|
||||
func floorToTimeSeriesBucket(ms int64) int64 {
|
||||
return ms - (ms % timeSeriesBucketMilli)
|
||||
}
|
||||
|
||||
func strictEffectiveFrom(sb *sqlbuilder.SelectBuilder, metricNames []string, effectiveFrom map[string]int64) string {
|
||||
names := make([]any, 0, len(metricNames))
|
||||
froms := make([]any, 0, len(metricNames))
|
||||
for _, name := range metricNames {
|
||||
names = append(names, name)
|
||||
froms = append(froms, effectiveFrom[name])
|
||||
}
|
||||
return "unix_milli >= transform(metric_name, " + sb.Var(names) + ", " + sb.Var(froms) + ", 0)"
|
||||
}
|
||||
|
||||
func (c *clickhouse) Sync(ctx context.Context, metricName string, labels []string, matchType string, effectiveFromMs int64, deleted bool, updatedAt time.Time) error {
|
||||
ctx = c.withThreads(ctx)
|
||||
|
||||
ib := sqlbuilder.NewInsertBuilder()
|
||||
ib.InsertInto(reductionRulesTable)
|
||||
ib.Cols("metric_name", "labels", "match_type", "effective_from_unix_milli", "deleted", "updated_at")
|
||||
ib.Values(metricName, labels, matchType, effectiveFromMs, deleted, updatedAt)
|
||||
|
||||
query, args := ib.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
if err := c.telemetryStore.ClickhouseDB().Exec(ctx, query, args...); err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "failed to sync reduction rule to clickhouse")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clickhouse) AttributeKeys(ctx context.Context, metricName string, startMs, endMs int64) ([]string, error) {
|
||||
ctx = c.withThreads(ctx)
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("attr_name")
|
||||
sb.Distinct()
|
||||
sb.From(metadataTable)
|
||||
sb.Where(
|
||||
sb.E("metric_name", metricName),
|
||||
"NOT startsWith(attr_name, '__')",
|
||||
sb.GE("last_reported_unix_milli", startMs),
|
||||
sb.LE("first_reported_unix_milli", endMs),
|
||||
)
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
rows, err := c.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to fetch metric attribute keys")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
keys := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var key string
|
||||
if err := rows.Scan(&key); err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan attribute key")
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys, rows.Err()
|
||||
}
|
||||
|
||||
func (c *clickhouse) EstimateCardinality(ctx context.Context, metricName string, keptLabels []string, startMs, endMs int64) (uint64, uint64, error) {
|
||||
ctx = c.withThreads(ctx)
|
||||
startMs = floorToTimeSeriesBucket(startMs)
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
reducedExpr := "1"
|
||||
if len(keptLabels) > 0 {
|
||||
reducedExpr = "uniq(("
|
||||
for i, label := range keptLabels {
|
||||
if i > 0 {
|
||||
reducedExpr += ", "
|
||||
}
|
||||
reducedExpr += "JSONExtractString(labels, " + sb.Var(label) + ")"
|
||||
}
|
||||
reducedExpr += "))"
|
||||
}
|
||||
|
||||
sb.Select("uniq(fingerprint)", reducedExpr)
|
||||
sb.From(bufferSeriesTable)
|
||||
conds := []string{
|
||||
sb.E("metric_name", metricName),
|
||||
sb.GE("unix_milli", startMs),
|
||||
sb.LT("unix_milli", endMs),
|
||||
sb.E("is_reduced", false),
|
||||
}
|
||||
sb.Where(conds...)
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
var current, reduced uint64
|
||||
if err := c.telemetryStore.ClickhouseDB().QueryRow(ctx, query, args...).Scan(¤t, &reduced); err != nil {
|
||||
return 0, 0, errors.WrapInternalf(err, errors.CodeInternal, "failed to estimate reduction impact")
|
||||
}
|
||||
if len(keptLabels) == 0 && current == 0 {
|
||||
reduced = 0
|
||||
}
|
||||
if reduced > current {
|
||||
reduced = current
|
||||
}
|
||||
return current, reduced, nil
|
||||
}
|
||||
|
||||
// VolumeByMetric returns ingested vs reduced series counts per metric.
|
||||
func (c *clickhouse) VolumeByMetric(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (map[string]volumeRow, error) {
|
||||
if len(metricNames) == 0 {
|
||||
return map[string]volumeRow{}, nil
|
||||
}
|
||||
ctx = c.withThreads(ctx)
|
||||
|
||||
ingested, err := c.ingestedSeriesCount(ctx, metricNames, effectiveFrom, startMs, endMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reduced, err := c.reducedSeriesCount(ctx, metricNames, effectiveFrom, startMs, endMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make(map[string]volumeRow, len(metricNames))
|
||||
for metricName, count := range ingested {
|
||||
out[metricName] = volumeRow{MetricName: metricName, Ingested: count, Reduced: out[metricName].Reduced}
|
||||
}
|
||||
for metricName, count := range reduced {
|
||||
row := out[metricName]
|
||||
row.MetricName = metricName
|
||||
row.Reduced = count
|
||||
out[metricName] = row
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ingestedSeriesCount counts distinct raw fingerprints per metric from the samples buffer over the
|
||||
// window.
|
||||
func (c *clickhouse) ingestedSeriesCount(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (map[string]uint64, error) {
|
||||
names := make([]any, len(metricNames))
|
||||
for i, name := range metricNames {
|
||||
names[i] = name
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("metric_name", "uniq(fingerprint)")
|
||||
sb.From(telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4BufferTableName)
|
||||
conds := []string{
|
||||
sb.In("metric_name", names...),
|
||||
sb.GE("unix_milli", startMs),
|
||||
sb.LT("unix_milli", endMs),
|
||||
}
|
||||
if len(effectiveFrom) > 0 {
|
||||
conds = append(conds, strictEffectiveFrom(sb, metricNames, effectiveFrom))
|
||||
}
|
||||
sb.Where(conds...)
|
||||
sb.GroupBy("metric_name")
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
rows, err := c.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to count ingested series")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make(map[string]uint64, len(metricNames))
|
||||
for rows.Next() {
|
||||
var (
|
||||
metricName string
|
||||
count uint64
|
||||
)
|
||||
if err := rows.Scan(&metricName, &count); err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan series count")
|
||||
}
|
||||
out[metricName] = count
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// reducedSeriesCount counts distinct reduced_fingerprints per metric, summed across the two 60s
|
||||
// reduced sample tables.
|
||||
func (c *clickhouse) reducedSeriesCount(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (map[string]uint64, error) {
|
||||
out := make(map[string]uint64, len(metricNames))
|
||||
for _, table := range []string{telemetrymetrics.SamplesV4ReducedLastTableName, telemetrymetrics.SamplesV4ReducedSumTableName} {
|
||||
counts, err := c.reducedSeriesCountForTable(ctx, telemetrymetrics.DBName+"."+table, metricNames, effectiveFrom, startMs, endMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for metricName, count := range counts {
|
||||
out[metricName] += count
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *clickhouse) reducedSeriesCountForTable(ctx context.Context, table string, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (map[string]uint64, error) {
|
||||
names := make([]any, len(metricNames))
|
||||
for i, name := range metricNames {
|
||||
names[i] = name
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("metric_name", "uniq(reduced_fingerprint)")
|
||||
sb.From(table)
|
||||
conds := []string{
|
||||
sb.In("metric_name", names...),
|
||||
sb.GE("unix_milli", startMs),
|
||||
sb.LT("unix_milli", endMs),
|
||||
}
|
||||
if len(effectiveFrom) > 0 {
|
||||
conds = append(conds, strictEffectiveFrom(sb, metricNames, effectiveFrom))
|
||||
}
|
||||
sb.Where(conds...)
|
||||
sb.GroupBy("metric_name")
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
rows, err := c.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to count reduced series")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make(map[string]uint64, len(metricNames))
|
||||
for rows.Next() {
|
||||
var (
|
||||
metricName string
|
||||
count uint64
|
||||
)
|
||||
if err := rows.Scan(&metricName, &count); err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan series count")
|
||||
}
|
||||
out[metricName] = count
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// RankByVolume ranks metrics by ingested/reduced series volume. Like VolumeByMetric, the counts read
|
||||
// the samples tables with a strict effective_from gate; the reduced count sums distinct
|
||||
// reduced_fingerprints across the two 60s reduced sample tables.
|
||||
func (c *clickhouse) RankByVolume(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, orderBy metricreductionruletypes.ReductionRuleOrderBy, order metricreductionruletypes.Order, startMs, endMs int64, offset, limit int) ([]volumeRow, error) {
|
||||
if len(metricNames) == 0 {
|
||||
return []volumeRow{}, nil
|
||||
}
|
||||
ctx = c.withThreads(ctx)
|
||||
|
||||
orderExpr := "ingested"
|
||||
switch orderBy {
|
||||
case metricreductionruletypes.OrderByReducedVolume:
|
||||
orderExpr = "reduced"
|
||||
case metricreductionruletypes.OrderByReduction:
|
||||
orderExpr = "if(ingested = 0, 0, (toFloat64(ingested) - toFloat64(reduced)) / toFloat64(ingested))"
|
||||
}
|
||||
direction := "ASC"
|
||||
if order == metricreductionruletypes.OrderDesc {
|
||||
direction = "DESC"
|
||||
}
|
||||
|
||||
ingestedTable := telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4BufferTableName
|
||||
reducedLast := telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4ReducedLastTableName
|
||||
reducedSum := telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4ReducedSumTableName
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("base.metric_name AS metric_name", "ifNull(i.cnt, 0) AS ingested", "ifNull(d.cnt, 0) AS reduced")
|
||||
sb.From("(SELECT arrayJoin(" + sb.Var(metricNames) + ") AS metric_name) AS base")
|
||||
sb.JoinWithOption(
|
||||
sqlbuilder.LeftJoin,
|
||||
"(SELECT metric_name, uniq(fingerprint) AS cnt FROM "+ingestedTable+" WHERE has("+sb.Var(metricNames)+", metric_name) AND unix_milli >= "+sb.Var(startMs)+" AND unix_milli < "+sb.Var(endMs)+" AND "+strictEffectiveFrom(sb, metricNames, effectiveFrom)+" GROUP BY metric_name) AS i",
|
||||
"base.metric_name = i.metric_name",
|
||||
)
|
||||
// Reduced series are spread across two type-specific tables; union the per-table distinct
|
||||
// reduced_fingerprints and sum per metric (a metric only lands in the table matching its type).
|
||||
sb.JoinWithOption(
|
||||
sqlbuilder.LeftJoin,
|
||||
"(SELECT metric_name, sum(cnt) AS cnt FROM ("+
|
||||
"SELECT metric_name, uniq(reduced_fingerprint) AS cnt FROM "+reducedLast+" WHERE has("+sb.Var(metricNames)+", metric_name) AND unix_milli >= "+sb.Var(startMs)+" AND unix_milli < "+sb.Var(endMs)+" AND "+strictEffectiveFrom(sb, metricNames, effectiveFrom)+" GROUP BY metric_name"+
|
||||
" UNION ALL "+
|
||||
"SELECT metric_name, uniq(reduced_fingerprint) AS cnt FROM "+reducedSum+" WHERE has("+sb.Var(metricNames)+", metric_name) AND unix_milli >= "+sb.Var(startMs)+" AND unix_milli < "+sb.Var(endMs)+" AND "+strictEffectiveFrom(sb, metricNames, effectiveFrom)+" GROUP BY metric_name"+
|
||||
") GROUP BY metric_name) AS d",
|
||||
"base.metric_name = d.metric_name",
|
||||
)
|
||||
sb.OrderBy(orderExpr + " " + direction)
|
||||
if limit > 0 {
|
||||
sb.Limit(limit).Offset(offset)
|
||||
}
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
rows, err := c.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to rank reduction rules by volume")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make([]volumeRow, 0, len(metricNames))
|
||||
for rows.Next() {
|
||||
var row volumeRow
|
||||
if err := rows.Scan(&row.MetricName, &row.Ingested, &row.Reduced); err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan volume row")
|
||||
}
|
||||
out = append(out, row)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (c *clickhouse) SampleVolume(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (uint64, uint64, error) {
|
||||
if len(metricNames) == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
ctx = c.withThreads(ctx)
|
||||
|
||||
ingested, err := c.countRawSamples(ctx, telemetrymetrics.DBName+"."+telemetrymetrics.SamplesV4BufferTableName, metricNames, effectiveFrom, startMs, endMs)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
last, err := c.countReducedSamples(ctx, telemetrymetrics.DBName+"."+telemetrymetrics.SamplesV4ReducedLastTableName, metricNames, effectiveFrom, startMs, endMs)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
sum, err := c.countReducedSamples(ctx, telemetrymetrics.DBName+"."+telemetrymetrics.SamplesV4ReducedSumTableName, metricNames, effectiveFrom, startMs, endMs)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return ingested, min(last+sum, ingested), nil
|
||||
}
|
||||
|
||||
func (c *clickhouse) countRawSamples(ctx context.Context, table string, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (uint64, error) {
|
||||
names := make([]any, len(metricNames))
|
||||
for i, name := range metricNames {
|
||||
names[i] = name
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("count()")
|
||||
sb.From(table)
|
||||
conds := []string{sb.In("metric_name", names...), sb.GE("unix_milli", startMs), sb.LT("unix_milli", endMs)}
|
||||
if len(effectiveFrom) > 0 {
|
||||
conds = append(conds, strictEffectiveFrom(sb, metricNames, effectiveFrom))
|
||||
}
|
||||
sb.Where(conds...)
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
var count uint64
|
||||
if err := c.telemetryStore.ClickhouseDB().QueryRow(ctx, query, args...).Scan(&count); err != nil {
|
||||
return 0, errors.WrapInternalf(err, errors.CodeInternal, "failed to count ingested samples")
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (c *clickhouse) countReducedSamples(ctx context.Context, table string, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (uint64, error) {
|
||||
names := make([]any, len(metricNames))
|
||||
for i, name := range metricNames {
|
||||
names[i] = name
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
// Reduced tables key the series on reduced_fingerprint (not fingerprint); dedupe ReplacingMergeTree recomputes.
|
||||
sb.Select("uniq(reduced_fingerprint, unix_milli)")
|
||||
sb.From(table)
|
||||
conds := []string{sb.In("metric_name", names...), sb.GE("unix_milli", startMs), sb.LT("unix_milli", endMs)}
|
||||
if len(effectiveFrom) > 0 {
|
||||
conds = append(conds, strictEffectiveFrom(sb, metricNames, effectiveFrom))
|
||||
}
|
||||
sb.Where(conds...)
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
var count uint64
|
||||
if err := c.telemetryStore.ClickhouseDB().QueryRow(ctx, query, args...).Scan(&count); err != nil {
|
||||
return 0, errors.WrapInternalf(err, errors.CodeInternal, "failed to count reduced samples")
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// SeriesTimeseries returns ingested vs reduced series per 60s bucket from the samples tables, gated
|
||||
// to each metric's strict effective_from (see strictEffectiveFrom).
|
||||
func (c *clickhouse) SeriesTimeseries(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) ([]volumePoint, error) {
|
||||
if len(metricNames) == 0 {
|
||||
return []volumePoint{}, nil
|
||||
}
|
||||
ctx = c.withThreads(ctx)
|
||||
|
||||
ingested, err := c.ingestedSeriesByBucket(ctx, metricNames, effectiveFrom, startMs, endMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reduced, err := c.reducedSeriesByBucket(ctx, metricNames, effectiveFrom, startMs, endMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mergeVolumePoints(ingested, reduced), nil
|
||||
}
|
||||
|
||||
func mergeVolumePoints(ingested, reduced map[int64]uint64) []volumePoint {
|
||||
buckets := make(map[int64]struct{}, len(ingested))
|
||||
for ts := range ingested {
|
||||
buckets[ts] = struct{}{}
|
||||
}
|
||||
for ts := range reduced {
|
||||
buckets[ts] = struct{}{}
|
||||
}
|
||||
timestamps := make([]int64, 0, len(buckets))
|
||||
for ts := range buckets {
|
||||
timestamps = append(timestamps, ts)
|
||||
}
|
||||
slices.Sort(timestamps)
|
||||
|
||||
points := make([]volumePoint, 0, len(timestamps))
|
||||
for _, ts := range timestamps {
|
||||
points = append(points, volumePoint{
|
||||
TimestampMs: ts,
|
||||
Ingested: ingested[ts],
|
||||
Reduced: reduced[ts],
|
||||
})
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
// ingestedSeriesByBucket counts distinct raw fingerprints per 60s bucket from the samples buffer.
|
||||
func (c *clickhouse) ingestedSeriesByBucket(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (map[int64]uint64, error) {
|
||||
names := make([]any, len(metricNames))
|
||||
for i, name := range metricNames {
|
||||
names[i] = name
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
bucketExpr := "intDiv(unix_milli, " + sb.Var(sampleBucketMilli) + ") * " + sb.Var(sampleBucketMilli) + " AS bucket"
|
||||
sb.Select(bucketExpr, "uniq(fingerprint)")
|
||||
sb.From(telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4BufferTableName)
|
||||
conds := []string{sb.In("metric_name", names...), sb.GE("unix_milli", startMs), sb.LT("unix_milli", endMs)}
|
||||
if len(effectiveFrom) > 0 {
|
||||
conds = append(conds, strictEffectiveFrom(sb, metricNames, effectiveFrom))
|
||||
}
|
||||
sb.Where(conds...)
|
||||
sb.GroupBy("bucket")
|
||||
|
||||
return c.scanBuckets(ctx, sb)
|
||||
}
|
||||
|
||||
// reducedSeriesByBucket counts distinct reduced_fingerprints per 60s bucket, summed across the two
|
||||
// reduced sample tables (a metric only lands in the table matching its type, so per-bucket sums are
|
||||
// exact).
|
||||
func (c *clickhouse) reducedSeriesByBucket(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (map[int64]uint64, error) {
|
||||
out := make(map[int64]uint64)
|
||||
for _, table := range []string{telemetrymetrics.SamplesV4ReducedLastTableName, telemetrymetrics.SamplesV4ReducedSumTableName} {
|
||||
names := make([]any, len(metricNames))
|
||||
for i, name := range metricNames {
|
||||
names[i] = name
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
bucketExpr := "intDiv(unix_milli, " + sb.Var(sampleBucketMilli) + ") * " + sb.Var(sampleBucketMilli) + " AS bucket"
|
||||
sb.Select(bucketExpr, "uniq(reduced_fingerprint)")
|
||||
sb.From(telemetrymetrics.DBName + "." + table)
|
||||
conds := []string{sb.In("metric_name", names...), sb.GE("unix_milli", startMs), sb.LT("unix_milli", endMs)}
|
||||
if len(effectiveFrom) > 0 {
|
||||
conds = append(conds, strictEffectiveFrom(sb, metricNames, effectiveFrom))
|
||||
}
|
||||
sb.Where(conds...)
|
||||
sb.GroupBy("bucket")
|
||||
|
||||
counts, err := c.scanBuckets(ctx, sb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for ts, count := range counts {
|
||||
out[ts] += count
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *clickhouse) scanBuckets(ctx context.Context, sb *sqlbuilder.SelectBuilder) (map[int64]uint64, error) {
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
rows, err := c.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to bucket series by time")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make(map[int64]uint64)
|
||||
for rows.Next() {
|
||||
var (
|
||||
ts int64
|
||||
count uint64
|
||||
)
|
||||
if err := rows.Scan(&ts, &count); err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan series bucket")
|
||||
}
|
||||
out[ts] = count
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
544
ee/modules/metricreductionrule/implmetricreductionrule/module.go
Normal file
544
ee/modules/metricreductionrule/implmetricreductionrule/module.go
Normal file
@@ -0,0 +1,544 @@
|
||||
package implmetricreductionrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
const (
|
||||
// effectiveFromMargin delays effective_from so the collector picks up the synced rule before it
|
||||
// goes live; it must be >= the collector's rule-refresh interval (see signoz-otel-collector#839).
|
||||
effectiveFromMargin = 5 * time.Minute
|
||||
defaultPreviewLookback = 24 * time.Hour
|
||||
|
||||
pricePerMillionSamplesUSD = 0.1
|
||||
monthDuration = 30 * 24 * time.Hour
|
||||
)
|
||||
|
||||
type module struct {
|
||||
store metricreductionruletypes.Store
|
||||
ch *clickhouse
|
||||
dashboard dashboard.Module
|
||||
ruleStore ruletypes.RuleStore
|
||||
licensing licensing.Licensing
|
||||
flagger flagger.Flagger
|
||||
metadataStore telemetrytypes.MetadataStore
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewModule(sqlStore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, dashboardModule dashboard.Module, queryParser queryparser.QueryParser, licensing licensing.Licensing, flagger flagger.Flagger, metadataStore telemetrytypes.MetadataStore, providerSettings factory.ProviderSettings, threads int) metricreductionrule.Module {
|
||||
scoped := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/modules/metricreductionrule/implmetricreductionrule")
|
||||
return &module{
|
||||
store: NewStore(sqlStore),
|
||||
ch: newClickhouse(telemetryStore, threads),
|
||||
dashboard: dashboardModule,
|
||||
ruleStore: sqlrulestore.NewRuleStore(sqlStore, queryParser, providerSettings),
|
||||
licensing: licensing,
|
||||
flagger: flagger,
|
||||
metadataStore: metadataStore,
|
||||
logger: scoped.Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *module) checkAccess(ctx context.Context, orgID valuer.UUID) error {
|
||||
if !m.flagger.BooleanOrEmpty(ctx, flagger.FeatureEnableMetricsReduction, featuretypes.NewFlaggerEvaluationContext(orgID)) {
|
||||
return errors.Newf(errors.TypeUnsupported, metricreductionruletypes.ErrCodeMetricReductionRuleUnsupported, "metric volume control is not enabled")
|
||||
}
|
||||
if _, err := m.licensing.GetActive(ctx, orgID); err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "metric volume control requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *module) List(ctx context.Context, orgID valuer.UUID, params *metricreductionruletypes.ListReductionRulesParams) (*metricreductionruletypes.GettableReductionRules, error) {
|
||||
if err := m.checkAccess(ctx, orgID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := params.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
startMs := now.Add(-defaultPreviewLookback).UnixMilli()
|
||||
endMs := now.UnixMilli()
|
||||
|
||||
switch params.OrderBy {
|
||||
case metricreductionruletypes.OrderByMetricName, metricreductionruletypes.OrderByLastUpdated:
|
||||
return m.listSortedByColumn(ctx, orgID, params, startMs, endMs)
|
||||
default:
|
||||
return m.listSortedByVolume(ctx, orgID, params, startMs, endMs)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *module) listSortedByColumn(ctx context.Context, orgID valuer.UUID, params *metricreductionruletypes.ListReductionRulesParams, startMs, endMs int64) (*metricreductionruletypes.GettableReductionRules, error) {
|
||||
domainRules, total, err := m.store.List(ctx, orgID, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metricNames := make([]string, len(domainRules))
|
||||
effectiveFrom := make(map[string]int64, len(domainRules))
|
||||
for i, rule := range domainRules {
|
||||
metricNames[i] = rule.MetricName
|
||||
effectiveFrom[rule.MetricName] = rule.EffectiveFrom.UnixMilli()
|
||||
}
|
||||
volumes, err := m.ch.VolumeByMetric(ctx, metricNames, effectiveFrom, startMs, endMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rules := make([]metricreductionruletypes.GettableReductionRule, 0, len(domainRules))
|
||||
for _, rule := range domainRules {
|
||||
rules = append(rules, withVolume(toGettableReductionRule(rule), volumes[rule.MetricName]))
|
||||
}
|
||||
|
||||
return &metricreductionruletypes.GettableReductionRules{Rules: rules, Total: total}, nil
|
||||
}
|
||||
|
||||
func (m *module) listSortedByVolume(ctx context.Context, orgID valuer.UUID, params *metricreductionruletypes.ListReductionRulesParams, startMs, endMs int64) (*metricreductionruletypes.GettableReductionRules, error) {
|
||||
allRules, total, err := m.store.List(ctx, orgID, &metricreductionruletypes.ListReductionRulesParams{Search: params.Search, MetricName: params.MetricName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if total == 0 {
|
||||
return &metricreductionruletypes.GettableReductionRules{Rules: []metricreductionruletypes.GettableReductionRule{}, Total: 0}, nil
|
||||
}
|
||||
|
||||
metricNames := make([]string, len(allRules))
|
||||
effectiveFrom := make(map[string]int64, len(allRules))
|
||||
ruleByMetric := make(map[string]*metricreductionruletypes.ReductionRule, len(allRules))
|
||||
for i, rule := range allRules {
|
||||
metricNames[i] = rule.MetricName
|
||||
effectiveFrom[rule.MetricName] = rule.EffectiveFrom.UnixMilli()
|
||||
ruleByMetric[rule.MetricName] = rule
|
||||
}
|
||||
|
||||
ranked, err := m.ch.RankByVolume(ctx, metricNames, effectiveFrom, params.OrderBy, params.Order, startMs, endMs, params.Offset, params.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rules := make([]metricreductionruletypes.GettableReductionRule, 0, len(ranked))
|
||||
for _, row := range ranked {
|
||||
rule, ok := ruleByMetric[row.MetricName]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rules = append(rules, withVolume(toGettableReductionRule(rule), row))
|
||||
}
|
||||
|
||||
return &metricreductionruletypes.GettableReductionRules{Rules: rules, Total: total}, nil
|
||||
}
|
||||
|
||||
func (m *module) Create(ctx context.Context, orgID valuer.UUID, userEmail string, req *metricreductionruletypes.PostableReductionRule) (*metricreductionruletypes.GettableReductionRule, error) {
|
||||
if err := m.checkAccess(ctx, orgID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.validateMetricForReduction(ctx, orgID, req.MetricName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
rule := metricreductionruletypes.NewReductionRule(orgID, req.MetricName, req.MatchType, req.Labels, now.Add(effectiveFromMargin), userEmail)
|
||||
|
||||
if err := m.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := m.store.Create(ctx, rule); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.ch.Sync(ctx, rule.MetricName, rule.Labels, rule.MatchType.StringValue(), rule.EffectiveFrom.UnixMilli(), false, rule.UpdatedAt)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gettable := toGettableReductionRule(rule)
|
||||
return &gettable, nil
|
||||
}
|
||||
|
||||
func (m *module) GetByID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*metricreductionruletypes.GettableReductionRule, error) {
|
||||
if err := m.checkAccess(ctx, orgID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rule, err := m.store.GetByID(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gettable := toGettableReductionRule(rule)
|
||||
return &gettable, nil
|
||||
}
|
||||
|
||||
func (m *module) UpdateByID(ctx context.Context, orgID valuer.UUID, userEmail string, id valuer.UUID, req *metricreductionruletypes.UpdatableReductionRule) (*metricreductionruletypes.GettableReductionRule, error) {
|
||||
if err := m.checkAccess(ctx, orgID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
existing, err := m.store.GetByID(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
existing.MatchType = req.MatchType
|
||||
existing.Labels = metricreductionruletypes.LabelList(req.Labels)
|
||||
existing.EffectiveFrom = now.Add(effectiveFromMargin)
|
||||
existing.UpdatedAt = now
|
||||
existing.UpdatedBy = userEmail
|
||||
|
||||
if err := m.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := m.store.Upsert(ctx, existing); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.ch.Sync(ctx, existing.MetricName, existing.Labels, existing.MatchType.StringValue(), existing.EffectiveFrom.UnixMilli(), false, existing.UpdatedAt)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gettable := toGettableReductionRule(existing)
|
||||
return &gettable, nil
|
||||
}
|
||||
|
||||
func (m *module) DeleteByID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
if err := m.checkAccess(ctx, orgID); err != nil {
|
||||
return err
|
||||
}
|
||||
rule, err := m.store.GetByID(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
effectiveFromMs := now.Add(effectiveFromMargin).UnixMilli()
|
||||
return m.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := m.store.DeleteByID(ctx, orgID, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.ch.Sync(ctx, rule.MetricName, []string{}, metricreductionruletypes.MatchTypeDrop.StringValue(), effectiveFromMs, true, now)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *module) Preview(ctx context.Context, orgID valuer.UUID, req *metricreductionruletypes.PostableReductionRulePreview) (*metricreductionruletypes.GettableReductionRulePreview, error) {
|
||||
if err := m.checkAccess(ctx, orgID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.validateMetricForReduction(ctx, orgID, req.MetricName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lookback := time.Duration(req.LookbackMs) * time.Millisecond
|
||||
if lookback <= 0 {
|
||||
lookback = defaultPreviewLookback
|
||||
}
|
||||
now := time.Now()
|
||||
startMs := now.Add(-lookback).UnixMilli()
|
||||
endMs := now.UnixMilli()
|
||||
current, reduced, reductionPercent, dropped, err := m.estimateVolume(ctx, req.MetricName, req.MatchType, req.Labels, startMs, endMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Baseline is what the metric keeps today (its current rule, or raw if none) so the preview reads
|
||||
// as current -> proposed.
|
||||
currentReduced := current
|
||||
if existing, gerr := m.store.Get(ctx, orgID, req.MetricName); gerr == nil {
|
||||
if _, existingReduced, _, _, eerr := m.estimateVolume(ctx, req.MetricName, existing.MatchType, existing.Labels, startMs, endMs); eerr == nil {
|
||||
currentReduced = existingReduced
|
||||
}
|
||||
}
|
||||
|
||||
return &metricreductionruletypes.GettableReductionRulePreview{
|
||||
IngestedSeries: current,
|
||||
CurrentRetainedSeries: currentReduced,
|
||||
RetainedSeries: reduced,
|
||||
ReductionPercent: reductionPercent,
|
||||
DroppedLabels: dropped,
|
||||
AffectedAssets: m.relatedAssetImpact(ctx, orgID, req.MetricName, dropped),
|
||||
EffectiveFrom: now.Add(effectiveFromMargin),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *module) Stats(ctx context.Context, orgID valuer.UUID) (*metricreductionruletypes.GettableReductionRuleStats, error) {
|
||||
if err := m.checkAccess(ctx, orgID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
startMs := now.Add(-defaultPreviewLookback).UnixMilli()
|
||||
endMs := now.UnixMilli()
|
||||
|
||||
allRules, total, err := m.store.List(ctx, orgID, &metricreductionruletypes.ListReductionRulesParams{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if total == 0 {
|
||||
return &metricreductionruletypes.GettableReductionRuleStats{}, nil
|
||||
}
|
||||
|
||||
metricNames := make([]string, len(allRules))
|
||||
effectiveFrom := make(map[string]int64, len(allRules))
|
||||
for i, rule := range allRules {
|
||||
metricNames[i] = rule.MetricName
|
||||
effectiveFrom[rule.MetricName] = rule.EffectiveFrom.UnixMilli()
|
||||
}
|
||||
|
||||
volumes, err := m.ch.VolumeByMetric(ctx, metricNames, effectiveFrom, startMs, endMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ingestedSeries, reducedSeries uint64
|
||||
for _, volume := range volumes {
|
||||
ingestedSeries += volume.Ingested
|
||||
reducedSeries += volume.Reduced
|
||||
}
|
||||
|
||||
ingestedSamples, reducedSamples, err := m.ch.SampleVolume(ctx, metricNames, effectiveFrom, startMs, endMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &metricreductionruletypes.GettableReductionRuleStats{
|
||||
IngestedSeries: ingestedSeries,
|
||||
RetainedSeries: reducedSeries,
|
||||
EstimatedMonthlySavingsUsd: monthlySavingsUSD(ingestedSamples, reducedSamples, startMs, endMs),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// monthlySavingsUSD extrapolates the windowed sample reduction to a monthly figure at the per-sample
|
||||
// list price. Ingested is gated to effective_from upstream, so pre-activation hours don't inflate it.
|
||||
func monthlySavingsUSD(ingestedSamples, reducedSamples uint64, startMs, endMs int64) float64 {
|
||||
if reducedSamples >= ingestedSamples || endMs <= startMs {
|
||||
return 0
|
||||
}
|
||||
savedSamples := float64(ingestedSamples - reducedSamples)
|
||||
monthlySamples := savedSamples * float64(monthDuration.Milliseconds()) / float64(endMs-startMs)
|
||||
return monthlySamples / 1_000_000 * pricePerMillionSamplesUSD
|
||||
}
|
||||
|
||||
func (m *module) Timeseries(ctx context.Context, orgID valuer.UUID) (*querybuildertypesv5.QueryRangeResponse, error) {
|
||||
if err := m.checkAccess(ctx, orgID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
startMs := now.Add(-defaultPreviewLookback).UnixMilli()
|
||||
endMs := now.UnixMilli()
|
||||
|
||||
allRules, _, err := m.store.List(ctx, orgID, &metricreductionruletypes.ListReductionRulesParams{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metricNames := make([]string, len(allRules))
|
||||
effectiveFrom := make(map[string]int64, len(allRules))
|
||||
for i, rule := range allRules {
|
||||
metricNames[i] = rule.MetricName
|
||||
effectiveFrom[rule.MetricName] = rule.EffectiveFrom.UnixMilli()
|
||||
}
|
||||
|
||||
points, err := m.ch.SeriesTimeseries(ctx, metricNames, effectiveFrom, startMs, endMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buildVolumeTimeseries(points), nil
|
||||
}
|
||||
|
||||
func buildVolumeTimeseries(points []volumePoint) *querybuildertypesv5.QueryRangeResponse {
|
||||
ingested := make([]*querybuildertypesv5.TimeSeriesValue, 0, len(points))
|
||||
reduced := make([]*querybuildertypesv5.TimeSeriesValue, 0, len(points))
|
||||
for _, point := range points {
|
||||
ingested = append(ingested, &querybuildertypesv5.TimeSeriesValue{Timestamp: point.TimestampMs, Value: float64(point.Ingested)})
|
||||
reduced = append(reduced, &querybuildertypesv5.TimeSeriesValue{Timestamp: point.TimestampMs, Value: float64(point.Reduced)})
|
||||
}
|
||||
|
||||
return &querybuildertypesv5.QueryRangeResponse{
|
||||
Type: querybuildertypesv5.RequestTypeTimeSeries,
|
||||
Data: querybuildertypesv5.QueryData{
|
||||
Results: []any{
|
||||
&querybuildertypesv5.TimeSeriesData{
|
||||
QueryName: "reduction_volume",
|
||||
Aggregations: []*querybuildertypesv5.AggregationBucket{
|
||||
{
|
||||
Series: []*querybuildertypesv5.TimeSeries{
|
||||
{Labels: []*querybuildertypesv5.Label{{Key: telemetrytypes.TelemetryFieldKey{Name: "series"}, Value: "ingested"}}, Values: ingested},
|
||||
{Labels: []*querybuildertypesv5.Label{{Key: telemetrytypes.TelemetryFieldKey{Name: "series"}, Value: "retained"}}, Values: reduced},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *module) validateMetricForReduction(ctx context.Context, orgID valuer.UUID, metricName string) error {
|
||||
now := time.Now()
|
||||
startTs := uint64(now.Add(-defaultPreviewLookback).UnixMilli())
|
||||
endTs := uint64(now.UnixMilli())
|
||||
|
||||
_, types, _, err := m.metadataStore.FetchTemporalityAndTypeMulti(ctx, orgID, startTs, endTs, metricName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metricType, ok := types[metricName]
|
||||
if !ok || metricType == metrictypes.UnspecifiedType {
|
||||
return errors.NewNotFoundf(errors.CodeNotFound, "metric not found: %q", metricName)
|
||||
}
|
||||
if metricType == metrictypes.ExpHistogramType {
|
||||
return errors.Newf(errors.TypeInvalidInput, metricreductionruletypes.ErrCodeMetricReductionRuleUnsupportedMetricType,
|
||||
"exponential histogram metrics cannot be reduced in v1")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *module) relatedAssetImpact(ctx context.Context, orgID valuer.UUID, metricName string, dropped []string) []metricreductionruletypes.AffectedAsset {
|
||||
affected := make([]metricreductionruletypes.AffectedAsset, 0)
|
||||
droppedSet := make(map[string]struct{}, len(dropped))
|
||||
for _, label := range dropped {
|
||||
droppedSet[label] = struct{}{}
|
||||
}
|
||||
|
||||
if dashboards, err := m.dashboard.GetByMetricNames(ctx, orgID, []string{metricName}); err != nil {
|
||||
m.logger.WarnContext(ctx, "failed to fetch related dashboards for reduction preview", slog.String("metric_name", metricName), errors.Attr(err))
|
||||
} else {
|
||||
for _, item := range dashboards[metricName] {
|
||||
usedLabels := append(splitCSV(item["group_by"]), splitCSV(item["filter_by"])...)
|
||||
affected = append(affected, metricreductionruletypes.AffectedAsset{
|
||||
Type: metricreductionruletypes.AssetTypeDashboard,
|
||||
ID: item["dashboard_id"],
|
||||
Name: item["dashboard_name"],
|
||||
Widget: &metricreductionruletypes.AffectedWidget{ID: item["widget_id"], Name: item["widget_name"]},
|
||||
ImpactedLabels: intersectLabels(usedLabels, droppedSet),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if alerts, err := m.ruleStore.GetStoredRulesByMetricName(ctx, orgID.String(), metricName); err != nil {
|
||||
m.logger.WarnContext(ctx, "failed to fetch related alerts for reduction preview", slog.String("metric_name", metricName), errors.Attr(err))
|
||||
} else {
|
||||
for _, a := range alerts {
|
||||
affected = append(affected, metricreductionruletypes.AffectedAsset{
|
||||
Type: metricreductionruletypes.AssetTypeAlert,
|
||||
ID: a.AlertID,
|
||||
Name: a.AlertName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return affected
|
||||
}
|
||||
|
||||
func toGettableReductionRule(rule *metricreductionruletypes.ReductionRule) metricreductionruletypes.GettableReductionRule {
|
||||
return metricreductionruletypes.GettableReductionRule{
|
||||
Identifiable: rule.Identifiable,
|
||||
TimeAuditable: rule.TimeAuditable,
|
||||
UserAuditable: rule.UserAuditable,
|
||||
MetricName: rule.MetricName,
|
||||
MatchType: rule.MatchType,
|
||||
Labels: rule.Labels,
|
||||
EffectiveFrom: rule.EffectiveFrom,
|
||||
Active: !rule.EffectiveFrom.After(time.Now()),
|
||||
}
|
||||
}
|
||||
|
||||
func withVolume(rule metricreductionruletypes.GettableReductionRule, volume volumeRow) metricreductionruletypes.GettableReductionRule {
|
||||
rule.IngestedSeries = volume.Ingested
|
||||
rule.RetainedSeries = volume.Reduced
|
||||
if volume.Ingested > 0 && volume.Reduced <= volume.Ingested {
|
||||
rule.ReductionPercent = (1 - float64(volume.Reduced)/float64(volume.Ingested)) * 100
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
func intersectLabels(keys []string, droppedSet map[string]struct{}) []string {
|
||||
seen := make(map[string]struct{})
|
||||
var out []string
|
||||
for _, key := range keys {
|
||||
if _, ok := droppedSet[key]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, dup := seen[key]; dup {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, key)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func splitCSV(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(s, ",")
|
||||
}
|
||||
|
||||
func resolveDroppedKept(matchType metricreductionruletypes.MatchType, ruleLabels, keys []string) (dropped, kept []string) {
|
||||
ruleSet := make(map[string]struct{}, len(ruleLabels))
|
||||
for _, l := range ruleLabels {
|
||||
ruleSet[l] = struct{}{}
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
if metricreductionruletypes.IsProtectedLabel(k) {
|
||||
kept = append(kept, k)
|
||||
continue
|
||||
}
|
||||
_, listed := ruleSet[k]
|
||||
drop := listed
|
||||
if matchType == metricreductionruletypes.MatchTypeKeep {
|
||||
drop = !listed
|
||||
}
|
||||
if drop {
|
||||
dropped = append(dropped, k)
|
||||
} else {
|
||||
kept = append(kept, k)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(dropped)
|
||||
sort.Strings(kept)
|
||||
return dropped, kept
|
||||
}
|
||||
|
||||
func (m *module) estimateVolume(ctx context.Context, metricName string, matchType metricreductionruletypes.MatchType, labels []string, startMs, endMs int64) (current uint64, reduced uint64, reductionPercent float64, dropped []string, err error) {
|
||||
keys, err := m.ch.AttributeKeys(ctx, metricName, startMs, endMs)
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, err
|
||||
}
|
||||
dropped, kept := resolveDroppedKept(matchType, labels, keys)
|
||||
|
||||
current, reduced, err = m.ch.EstimateCardinality(ctx, metricName, kept, startMs, endMs)
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, err
|
||||
}
|
||||
if current > 0 && reduced <= current {
|
||||
reductionPercent = (1 - float64(reduced)/float64(current)) * 100
|
||||
}
|
||||
return current, reduced, reductionPercent, dropped, nil
|
||||
}
|
||||
145
ee/modules/metricreductionrule/implmetricreductionrule/store.go
Normal file
145
ee/modules/metricreductionrule/implmetricreductionrule/store.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package implmetricreductionrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type store struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewStore(sqlstore sqlstore.SQLStore) metricreductionruletypes.Store {
|
||||
return &store{sqlstore: sqlstore}
|
||||
}
|
||||
|
||||
func (s *store) List(ctx context.Context, orgID valuer.UUID, params *metricreductionruletypes.ListReductionRulesParams) ([]*metricreductionruletypes.ReductionRule, int, error) {
|
||||
column := "metric_name"
|
||||
if params.OrderBy == metricreductionruletypes.OrderByLastUpdated {
|
||||
column = "updated_at"
|
||||
}
|
||||
direction := "ASC"
|
||||
if params.Order == metricreductionruletypes.OrderDesc {
|
||||
direction = "DESC"
|
||||
}
|
||||
|
||||
rules := make([]*metricreductionruletypes.ReductionRule, 0)
|
||||
query := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(&rules).
|
||||
Where("org_id = ?", orgID).
|
||||
Order(column + " " + direction)
|
||||
if params.Search != "" {
|
||||
query = query.Where("metric_name LIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
if params.MetricName != "" {
|
||||
query = query.Where("metric_name = ?", params.MetricName)
|
||||
}
|
||||
if params.Limit > 0 {
|
||||
query = query.Limit(params.Limit).Offset(params.Offset)
|
||||
}
|
||||
|
||||
total, err := query.ScanAndCount(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return rules, total, nil
|
||||
}
|
||||
|
||||
func (s *store) Get(ctx context.Context, orgID valuer.UUID, metricName string) (*metricreductionruletypes.ReductionRule, error) {
|
||||
rule := new(metricreductionruletypes.ReductionRule)
|
||||
err := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(rule).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("metric_name = ?", metricName).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, metricreductionruletypes.ErrCodeMetricReductionRuleNotFound, "no reduction rule found for metric %q", metricName)
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
func (s *store) GetByID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*metricreductionruletypes.ReductionRule, error) {
|
||||
rule := new(metricreductionruletypes.ReductionRule)
|
||||
err := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(rule).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("id = ?", id).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, metricreductionruletypes.ErrCodeMetricReductionRuleNotFound, "no reduction rule found with id %q", id.String())
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
func (s *store) Create(ctx context.Context, rule *metricreductionruletypes.ReductionRule) error {
|
||||
res, err := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(rule).
|
||||
On("CONFLICT (org_id, metric_name) DO NOTHING").
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
return errors.Newf(errors.TypeAlreadyExists, metricreductionruletypes.ErrCodeMetricReductionRuleAlreadyExists,
|
||||
"a reduction rule for metric %q already exists", rule.MetricName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *store) Upsert(ctx context.Context, rule *metricreductionruletypes.ReductionRule) error {
|
||||
_, err := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(rule).
|
||||
On("CONFLICT (org_id, metric_name) DO UPDATE").
|
||||
Set("match_type = EXCLUDED.match_type").
|
||||
Set("labels = EXCLUDED.labels").
|
||||
Set("effective_from = EXCLUDED.effective_from").
|
||||
Set("updated_at = EXCLUDED.updated_at").
|
||||
Set("updated_by = EXCLUDED.updated_by").
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) DeleteByID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
res, err := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model((*metricreductionruletypes.ReductionRule)(nil)).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("id = ?", id).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
return errors.Newf(errors.TypeNotFound, metricreductionruletypes.ErrCodeMetricReductionRuleNotFound, "no reduction rule found with id %q", id.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *store) RunInTx(ctx context.Context, cb func(ctx context.Context) error) error {
|
||||
return s.sqlstore.RunInTxCtx(ctx, nil, cb)
|
||||
}
|
||||
@@ -107,6 +107,15 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
Route: "",
|
||||
})
|
||||
|
||||
metricsReduction := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureEnableMetricsReduction, evalCtx)
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureEnableMetricsReduction.String()),
|
||||
Active: metricsReduction,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
})
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
|
||||
@@ -90,12 +90,8 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
|
||||
// initiate agent config handler
|
||||
agentConfMgr, err := agentConf.Initiate(&agentConf.ManagerOptions{
|
||||
Store: signoz.SQLStore,
|
||||
AgentFeatures: []agentConf.AgentFeature{
|
||||
logParsingPipelineController,
|
||||
signoz.Modules.SpanMapper,
|
||||
signoz.Modules.LLMPricingRule,
|
||||
},
|
||||
Store: signoz.SQLStore,
|
||||
AgentFeatures: []agentConf.AgentFeature{logParsingPipelineController},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -4,22 +4,14 @@
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { useMutation } from 'react-query';
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
GetChecks200,
|
||||
GetChecksParams,
|
||||
InframonitoringtypesPostableClustersDTO,
|
||||
InframonitoringtypesPostableDaemonSetsDTO,
|
||||
InframonitoringtypesPostableDeploymentsDTO,
|
||||
@@ -47,94 +39,7 @@ import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* Checks whether the metrics and attributes required to power the infra-monitoring section selected by the 'type' query parameter (hosts, processes, pods, nodes, deployments, daemonsets, statefulsets, jobs, namespaces, clusters, volumes) are being received. For each collector receiver or processor that contributes required metrics or attributes, lists what is present and what is missing, with a prebuilt user-facing message and a docs link per missing component. Default-enabled metrics are those expected as soon as the receiver is configured; optional metrics require 'enabled: true' in receiver config. 'ready' is true only when every missing list is empty.
|
||||
* @summary Run Infra Monitoring Setup Checks
|
||||
*/
|
||||
export const getChecks = (params: GetChecksParams, signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<GetChecks200>({
|
||||
url: `/api/v2/infra_monitoring/checks`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetChecksQueryKey = (params?: GetChecksParams) => {
|
||||
return [
|
||||
`/api/v2/infra_monitoring/checks`,
|
||||
...(params ? [params] : []),
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const getGetChecksQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getChecks>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: GetChecksParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof getChecks>>, TError, TData>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetChecksQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getChecks>>> = ({
|
||||
signal,
|
||||
}) => getChecks(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getChecks>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetChecksQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getChecks>>
|
||||
>;
|
||||
export type GetChecksQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Run Infra Monitoring Setup Checks
|
||||
*/
|
||||
|
||||
export function useGetChecks<
|
||||
TData = Awaited<ReturnType<typeof getChecks>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: GetChecksParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof getChecks>>, TError, TData>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetChecksQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Run Infra Monitoring Setup Checks
|
||||
*/
|
||||
export const invalidateGetChecks = async (
|
||||
queryClient: QueryClient,
|
||||
params: GetChecksParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetChecksQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = (
|
||||
@@ -217,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 = (
|
||||
@@ -300,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 = (
|
||||
@@ -383,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 = (
|
||||
@@ -466,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 = (
|
||||
@@ -549,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 = (
|
||||
@@ -632,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 = (
|
||||
@@ -715,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 = (
|
||||
@@ -798,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 = (
|
||||
@@ -881,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 = (
|
||||
|
||||
@@ -18,6 +18,8 @@ import type {
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
CreateMetricReductionRule201,
|
||||
DeleteMetricReductionRuleByIDPathParameters,
|
||||
GetMetricAlerts200,
|
||||
GetMetricAlertsParams,
|
||||
GetMetricAttributes200,
|
||||
@@ -28,22 +30,762 @@ import type {
|
||||
GetMetricHighlightsParams,
|
||||
GetMetricMetadata200,
|
||||
GetMetricMetadataParams,
|
||||
GetMetricReductionRuleByID200,
|
||||
GetMetricReductionRuleByIDPathParameters,
|
||||
GetMetricReductionRuleStats200,
|
||||
GetMetricReductionRuleTimeseries200,
|
||||
GetMetricsOnboardingStatus200,
|
||||
GetMetricsStats200,
|
||||
GetMetricsTreemap200,
|
||||
InspectMetrics200,
|
||||
ListMetricReductionRules200,
|
||||
ListMetricReductionRulesParams,
|
||||
ListMetrics200,
|
||||
ListMetricsParams,
|
||||
MetricreductionruletypesPostableReductionRuleDTO,
|
||||
MetricreductionruletypesPostableReductionRulePreviewDTO,
|
||||
MetricreductionruletypesUpdatableReductionRuleDTO,
|
||||
MetricsexplorertypesInspectMetricsRequestDTO,
|
||||
MetricsexplorertypesStatsRequestDTO,
|
||||
MetricsexplorertypesTreemapRequestDTO,
|
||||
MetricsexplorertypesUpdateMetricMetadataRequestDTO,
|
||||
PreviewMetricReductionRule200,
|
||||
RenderErrorResponseDTO,
|
||||
UpdateMetricReductionRuleByID200,
|
||||
UpdateMetricReductionRuleByIDPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* Returns active metric volume-control (label reduction) rules.
|
||||
* @summary List metric reduction rules
|
||||
*/
|
||||
export const listMetricReductionRules = (
|
||||
params?: ListMetricReductionRulesParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListMetricReductionRules200>({
|
||||
url: `/api/v2/metric_reduction_rules`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListMetricReductionRulesQueryKey = (
|
||||
params?: ListMetricReductionRulesParams,
|
||||
) => {
|
||||
return [
|
||||
`/api/v2/metric_reduction_rules`,
|
||||
...(params ? [params] : []),
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const getListMetricReductionRulesQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listMetricReductionRules>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListMetricReductionRulesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listMetricReductionRules>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getListMetricReductionRulesQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof listMetricReductionRules>>
|
||||
> = ({ signal }) => listMetricReductionRules(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listMetricReductionRules>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListMetricReductionRulesQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listMetricReductionRules>>
|
||||
>;
|
||||
export type ListMetricReductionRulesQueryError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List metric reduction rules
|
||||
*/
|
||||
|
||||
export function useListMetricReductionRules<
|
||||
TData = Awaited<ReturnType<typeof listMetricReductionRules>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListMetricReductionRulesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listMetricReductionRules>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListMetricReductionRulesQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List metric reduction rules
|
||||
*/
|
||||
export const invalidateListMetricReductionRules = async (
|
||||
queryClient: QueryClient,
|
||||
params?: ListMetricReductionRulesParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListMetricReductionRulesQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a volume-control rule for a metric and returns it with its id; fails if the metric already has a rule.
|
||||
* @summary Create a metric reduction rule
|
||||
*/
|
||||
export const createMetricReductionRule = (
|
||||
metricreductionruletypesPostableReductionRuleDTO?: BodyType<MetricreductionruletypesPostableReductionRuleDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateMetricReductionRule201>({
|
||||
url: `/api/v2/metric_reduction_rules`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: metricreductionruletypesPostableReductionRuleDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateMetricReductionRuleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createMetricReductionRule>>,
|
||||
TError,
|
||||
{ data?: BodyType<MetricreductionruletypesPostableReductionRuleDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createMetricReductionRule>>,
|
||||
TError,
|
||||
{ data?: BodyType<MetricreductionruletypesPostableReductionRuleDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createMetricReductionRule'];
|
||||
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 createMetricReductionRule>>,
|
||||
{ data?: BodyType<MetricreductionruletypesPostableReductionRuleDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createMetricReductionRule(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateMetricReductionRuleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createMetricReductionRule>>
|
||||
>;
|
||||
export type CreateMetricReductionRuleMutationBody =
|
||||
| BodyType<MetricreductionruletypesPostableReductionRuleDTO>
|
||||
| undefined;
|
||||
export type CreateMetricReductionRuleMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create a metric reduction rule
|
||||
*/
|
||||
export const useCreateMetricReductionRule = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createMetricReductionRule>>,
|
||||
TError,
|
||||
{ data?: BodyType<MetricreductionruletypesPostableReductionRuleDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createMetricReductionRule>>,
|
||||
TError,
|
||||
{ data?: BodyType<MetricreductionruletypesPostableReductionRuleDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateMetricReductionRuleMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Deletes a volume-control rule by its id.
|
||||
* @summary Delete a metric reduction rule by id
|
||||
*/
|
||||
export const deleteMetricReductionRuleByID = (
|
||||
{ id }: DeleteMetricReductionRuleByIDPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v2/metric_reduction_rules/${id}`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteMetricReductionRuleByIDMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteMetricReductionRuleByID>>,
|
||||
TError,
|
||||
{ pathParams: DeleteMetricReductionRuleByIDPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteMetricReductionRuleByID>>,
|
||||
TError,
|
||||
{ pathParams: DeleteMetricReductionRuleByIDPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteMetricReductionRuleByID'];
|
||||
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 deleteMetricReductionRuleByID>>,
|
||||
{ pathParams: DeleteMetricReductionRuleByIDPathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteMetricReductionRuleByID(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteMetricReductionRuleByIDMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteMetricReductionRuleByID>>
|
||||
>;
|
||||
|
||||
export type DeleteMetricReductionRuleByIDMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete a metric reduction rule by id
|
||||
*/
|
||||
export const useDeleteMetricReductionRuleByID = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteMetricReductionRuleByID>>,
|
||||
TError,
|
||||
{ pathParams: DeleteMetricReductionRuleByIDPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteMetricReductionRuleByID>>,
|
||||
TError,
|
||||
{ pathParams: DeleteMetricReductionRuleByIDPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteMetricReductionRuleByIDMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Returns a single volume-control rule by its id.
|
||||
* @summary Get a metric reduction rule by id
|
||||
*/
|
||||
export const getMetricReductionRuleByID = (
|
||||
{ id }: GetMetricReductionRuleByIDPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetMetricReductionRuleByID200>({
|
||||
url: `/api/v2/metric_reduction_rules/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetMetricReductionRuleByIDQueryKey = ({
|
||||
id,
|
||||
}: GetMetricReductionRuleByIDPathParameters) => {
|
||||
return [`/api/v2/metric_reduction_rules/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetMetricReductionRuleByIDQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMetricReductionRuleByID>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetMetricReductionRuleByIDPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleByID>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetMetricReductionRuleByIDQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleByID>>
|
||||
> = ({ signal }) => getMetricReductionRuleByID({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleByID>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetMetricReductionRuleByIDQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleByID>>
|
||||
>;
|
||||
export type GetMetricReductionRuleByIDQueryError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get a metric reduction rule by id
|
||||
*/
|
||||
|
||||
export function useGetMetricReductionRuleByID<
|
||||
TData = Awaited<ReturnType<typeof getMetricReductionRuleByID>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetMetricReductionRuleByIDPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleByID>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetMetricReductionRuleByIDQueryOptions(
|
||||
{ id },
|
||||
options,
|
||||
);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get a metric reduction rule by id
|
||||
*/
|
||||
export const invalidateGetMetricReductionRuleByID = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetMetricReductionRuleByIDPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetMetricReductionRuleByIDQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the match type and labels of a volume-control rule by its id; the metric name is immutable.
|
||||
* @summary Update a metric reduction rule by id
|
||||
*/
|
||||
export const updateMetricReductionRuleByID = (
|
||||
{ id }: UpdateMetricReductionRuleByIDPathParameters,
|
||||
metricreductionruletypesUpdatableReductionRuleDTO?: BodyType<MetricreductionruletypesUpdatableReductionRuleDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<UpdateMetricReductionRuleByID200>({
|
||||
url: `/api/v2/metric_reduction_rules/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: metricreductionruletypesUpdatableReductionRuleDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateMetricReductionRuleByIDMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMetricReductionRuleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateMetricReductionRuleByIDPathParameters;
|
||||
data?: BodyType<MetricreductionruletypesUpdatableReductionRuleDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMetricReductionRuleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateMetricReductionRuleByIDPathParameters;
|
||||
data?: BodyType<MetricreductionruletypesUpdatableReductionRuleDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateMetricReductionRuleByID'];
|
||||
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 updateMetricReductionRuleByID>>,
|
||||
{
|
||||
pathParams: UpdateMetricReductionRuleByIDPathParameters;
|
||||
data?: BodyType<MetricreductionruletypesUpdatableReductionRuleDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateMetricReductionRuleByID(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateMetricReductionRuleByIDMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateMetricReductionRuleByID>>
|
||||
>;
|
||||
export type UpdateMetricReductionRuleByIDMutationBody =
|
||||
| BodyType<MetricreductionruletypesUpdatableReductionRuleDTO>
|
||||
| undefined;
|
||||
export type UpdateMetricReductionRuleByIDMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update a metric reduction rule by id
|
||||
*/
|
||||
export const useUpdateMetricReductionRuleByID = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMetricReductionRuleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateMetricReductionRuleByIDPathParameters;
|
||||
data?: BodyType<MetricreductionruletypesUpdatableReductionRuleDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateMetricReductionRuleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateMetricReductionRuleByIDPathParameters;
|
||||
data?: BodyType<MetricreductionruletypesUpdatableReductionRuleDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateMetricReductionRuleByIDMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Estimates the series reduction and related-asset impact of a candidate volume-control rule without persisting it.
|
||||
* @summary Preview a metric reduction rule
|
||||
*/
|
||||
export const previewMetricReductionRule = (
|
||||
metricreductionruletypesPostableReductionRulePreviewDTO?: BodyType<MetricreductionruletypesPostableReductionRulePreviewDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<PreviewMetricReductionRule200>({
|
||||
url: `/api/v2/metric_reduction_rules/preview`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: metricreductionruletypesPostableReductionRulePreviewDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPreviewMetricReductionRuleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof previewMetricReductionRule>>,
|
||||
TError,
|
||||
{ data?: BodyType<MetricreductionruletypesPostableReductionRulePreviewDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof previewMetricReductionRule>>,
|
||||
TError,
|
||||
{ data?: BodyType<MetricreductionruletypesPostableReductionRulePreviewDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['previewMetricReductionRule'];
|
||||
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 previewMetricReductionRule>>,
|
||||
{ data?: BodyType<MetricreductionruletypesPostableReductionRulePreviewDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return previewMetricReductionRule(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type PreviewMetricReductionRuleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof previewMetricReductionRule>>
|
||||
>;
|
||||
export type PreviewMetricReductionRuleMutationBody =
|
||||
| BodyType<MetricreductionruletypesPostableReductionRulePreviewDTO>
|
||||
| undefined;
|
||||
export type PreviewMetricReductionRuleMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Preview a metric reduction rule
|
||||
*/
|
||||
export const usePreviewMetricReductionRule = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof previewMetricReductionRule>>,
|
||||
TError,
|
||||
{ data?: BodyType<MetricreductionruletypesPostableReductionRulePreviewDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof previewMetricReductionRule>>,
|
||||
TError,
|
||||
{ data?: BodyType<MetricreductionruletypesPostableReductionRulePreviewDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getPreviewMetricReductionRuleMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Returns total ingested vs retained series and the estimated monthly savings across all volume-control rules.
|
||||
* @summary Metric reduction stats
|
||||
*/
|
||||
export const getMetricReductionRuleStats = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<GetMetricReductionRuleStats200>({
|
||||
url: `/api/v2/metric_reduction_rules/stats`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetMetricReductionRuleStatsQueryKey = () => {
|
||||
return [`/api/v2/metric_reduction_rules/stats`] as const;
|
||||
};
|
||||
|
||||
export const getGetMetricReductionRuleStatsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMetricReductionRuleStats>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleStats>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetMetricReductionRuleStatsQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleStats>>
|
||||
> = ({ signal }) => getMetricReductionRuleStats(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleStats>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetMetricReductionRuleStatsQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleStats>>
|
||||
>;
|
||||
export type GetMetricReductionRuleStatsQueryError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Metric reduction stats
|
||||
*/
|
||||
|
||||
export function useGetMetricReductionRuleStats<
|
||||
TData = Awaited<ReturnType<typeof getMetricReductionRuleStats>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleStats>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetMetricReductionRuleStatsQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Metric reduction stats
|
||||
*/
|
||||
export const invalidateGetMetricReductionRuleStats = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetMetricReductionRuleStatsQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns ingested vs retained series over time across all volume-control rules (hourly buckets), in the query-range time-series response shape.
|
||||
* @summary Metric reduction volume over time
|
||||
*/
|
||||
export const getMetricReductionRuleTimeseries = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<GetMetricReductionRuleTimeseries200>({
|
||||
url: `/api/v2/metric_reduction_rules/timeseries`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetMetricReductionRuleTimeseriesQueryKey = () => {
|
||||
return [`/api/v2/metric_reduction_rules/timeseries`] as const;
|
||||
};
|
||||
|
||||
export const getGetMetricReductionRuleTimeseriesQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMetricReductionRuleTimeseries>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleTimeseries>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetMetricReductionRuleTimeseriesQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleTimeseries>>
|
||||
> = ({ signal }) => getMetricReductionRuleTimeseries(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleTimeseries>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetMetricReductionRuleTimeseriesQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleTimeseries>>
|
||||
>;
|
||||
export type GetMetricReductionRuleTimeseriesQueryError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Metric reduction volume over time
|
||||
*/
|
||||
|
||||
export function useGetMetricReductionRuleTimeseries<
|
||||
TData = Awaited<ReturnType<typeof getMetricReductionRuleTimeseries>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricReductionRuleTimeseries>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetMetricReductionRuleTimeseriesQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Metric reduction volume over time
|
||||
*/
|
||||
export const invalidateGetMetricReductionRuleTimeseries = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetMetricReductionRuleTimeseriesQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint returns a list of distinct metric names within the specified time range
|
||||
* @summary List metric names
|
||||
|
||||
@@ -5458,121 +5458,6 @@ export interface GlobaltypesConfigDTO {
|
||||
mcp_url: string | null;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesCheckComponentTypeDTO {
|
||||
receiver = 'receiver',
|
||||
processor = 'processor',
|
||||
}
|
||||
export interface InframonitoringtypesAssociatedComponentDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
type: InframonitoringtypesCheckComponentTypeDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesAttributesComponentEntryDTO {
|
||||
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
attributes: string[] | null;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesCheckTypeDTO {
|
||||
hosts = 'hosts',
|
||||
processes = 'processes',
|
||||
pods = 'pods',
|
||||
nodes = 'nodes',
|
||||
deployments = 'deployments',
|
||||
daemonsets = 'daemonsets',
|
||||
statefulsets = 'statefulsets',
|
||||
jobs = 'jobs',
|
||||
namespaces = 'namespaces',
|
||||
clusters = 'clusters',
|
||||
volumes = 'volumes',
|
||||
}
|
||||
export interface InframonitoringtypesMissingMetricsComponentEntryDTO {
|
||||
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
documentationLink: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
metrics: string[] | null;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesMissingAttributesComponentEntryDTO {
|
||||
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
attributes: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
documentationLink: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesMetricsComponentEntryDTO {
|
||||
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
metrics: string[] | null;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesChecksDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
missingDefaultEnabledMetrics:
|
||||
| InframonitoringtypesMissingMetricsComponentEntryDTO[]
|
||||
| null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
missingOptionalMetrics:
|
||||
| InframonitoringtypesMissingMetricsComponentEntryDTO[]
|
||||
| null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
missingRequiredAttributes:
|
||||
| InframonitoringtypesMissingAttributesComponentEntryDTO[]
|
||||
| null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
presentDefaultEnabledMetrics:
|
||||
| InframonitoringtypesMetricsComponentEntryDTO[]
|
||||
| null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
presentOptionalMetrics: InframonitoringtypesMetricsComponentEntryDTO[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
presentRequiredAttributes:
|
||||
| InframonitoringtypesAttributesComponentEntryDTO[]
|
||||
| null;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
ready: boolean;
|
||||
type: InframonitoringtypesCheckTypeDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesClusterRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -5650,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',
|
||||
@@ -5685,6 +5577,7 @@ export interface InframonitoringtypesClustersDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesClusterRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -5762,6 +5655,7 @@ export interface InframonitoringtypesDaemonSetsDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesDaemonSetRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -5839,6 +5733,7 @@ export interface InframonitoringtypesDeploymentsDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesDeploymentRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -5924,6 +5819,7 @@ export interface InframonitoringtypesHostsDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesHostRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -6009,6 +5905,7 @@ export interface InframonitoringtypesJobsDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesJobRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -6058,6 +5955,7 @@ export interface InframonitoringtypesNamespacesDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesNamespaceRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -6124,6 +6022,7 @@ export interface InframonitoringtypesNodesDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesNodeRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -6207,6 +6106,7 @@ export interface InframonitoringtypesPodsDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesPodRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -6554,6 +6454,7 @@ export interface InframonitoringtypesStatefulSetsDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesStatefulSetRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -6622,6 +6523,7 @@ export interface InframonitoringtypesVolumesDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesVolumeRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -6787,6 +6689,213 @@ export interface LlmpricingruletypesUpdatableLLMPricingRulesDTO {
|
||||
rules: LlmpricingruletypesUpdatableLLMPricingRuleDTO[] | null;
|
||||
}
|
||||
|
||||
export enum MetricreductionruletypesAssetTypeDTO {
|
||||
dashboard = 'dashboard',
|
||||
alert_rule = 'alert_rule',
|
||||
}
|
||||
export interface MetricreductionruletypesAffectedWidgetDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface MetricreductionruletypesAffectedAssetDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
impactedLabels: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
type: MetricreductionruletypesAssetTypeDTO;
|
||||
widget?: MetricreductionruletypesAffectedWidgetDTO;
|
||||
}
|
||||
|
||||
export enum MetricreductionruletypesMatchTypeDTO {
|
||||
drop = 'drop',
|
||||
keep = 'keep',
|
||||
}
|
||||
export interface MetricreductionruletypesGettableReductionRuleDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
active: boolean;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
effectiveFrom: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
ingestedSeries: number;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
labels: string[] | null;
|
||||
matchType: MetricreductionruletypesMatchTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
metricName: string;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
reductionPercent: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
retainedSeries: number;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface MetricreductionruletypesGettableReductionRulePreviewDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
affectedAssets: MetricreductionruletypesAffectedAssetDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
currentRetainedSeries: number;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
droppedLabels: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
effectiveFrom: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
ingestedSeries: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
reductionPercent: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
retainedSeries: number;
|
||||
}
|
||||
|
||||
export interface MetricreductionruletypesGettableReductionRuleStatsDTO {
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
estimatedMonthlySavingsUsd: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
ingestedSeries: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
retainedSeries: number;
|
||||
}
|
||||
|
||||
export interface MetricreductionruletypesGettableReductionRulesDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
rules: MetricreductionruletypesGettableReductionRuleDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
total: number;
|
||||
}
|
||||
|
||||
export enum MetricreductionruletypesOrderDTO {
|
||||
asc = 'asc',
|
||||
desc = 'desc',
|
||||
}
|
||||
export interface MetricreductionruletypesPostableReductionRuleDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
labels: string[] | null;
|
||||
matchType: MetricreductionruletypesMatchTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
metricName: string;
|
||||
}
|
||||
|
||||
export interface MetricreductionruletypesPostableReductionRulePreviewDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
labels: string[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
lookbackMs?: number;
|
||||
matchType: MetricreductionruletypesMatchTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
metricName: string;
|
||||
}
|
||||
|
||||
export enum MetricreductionruletypesReductionRuleOrderByDTO {
|
||||
metric = 'metric',
|
||||
ingested_volume = 'ingested_volume',
|
||||
reduced_volume = 'reduced_volume',
|
||||
reduction = 'reduction',
|
||||
last_updated = 'last_updated',
|
||||
}
|
||||
export interface MetricreductionruletypesUpdatableReductionRuleDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
labels: string[] | null;
|
||||
matchType: MetricreductionruletypesMatchTypeDTO;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesInspectMetricsRequestDTO {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -10344,21 +10453,6 @@ export type Healthz503 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetChecksParams = {
|
||||
/**
|
||||
* @description undefined
|
||||
*/
|
||||
type: InframonitoringtypesCheckTypeDTO;
|
||||
};
|
||||
|
||||
export type GetChecks200 = {
|
||||
data: InframonitoringtypesChecksDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListClusters200 = {
|
||||
data: InframonitoringtypesClustersDTO;
|
||||
/**
|
||||
@@ -10447,6 +10541,102 @@ export type Livez200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListMetricReductionRulesParams = {
|
||||
/**
|
||||
* @description undefined
|
||||
*/
|
||||
orderBy?: MetricreductionruletypesReductionRuleOrderByDTO;
|
||||
/**
|
||||
* @description undefined
|
||||
*/
|
||||
order?: MetricreductionruletypesOrderDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
search?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
metricName?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export type ListMetricReductionRules200 = {
|
||||
data: MetricreductionruletypesGettableReductionRulesDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateMetricReductionRule201 = {
|
||||
data: MetricreductionruletypesGettableReductionRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteMetricReductionRuleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetMetricReductionRuleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetMetricReductionRuleByID200 = {
|
||||
data: MetricreductionruletypesGettableReductionRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateMetricReductionRuleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type UpdateMetricReductionRuleByID200 = {
|
||||
data: MetricreductionruletypesGettableReductionRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type PreviewMetricReductionRule200 = {
|
||||
data: MetricreductionruletypesGettableReductionRulePreviewDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMetricReductionRuleStats200 = {
|
||||
data: MetricreductionruletypesGettableReductionRuleStatsDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMetricReductionRuleTimeseries200 = {
|
||||
data: Querybuildertypesv5QueryRangeResponseDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListMetricsParams = {
|
||||
/**
|
||||
* @type integer,null
|
||||
|
||||
@@ -794,13 +794,6 @@ 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 {
|
||||
|
||||
@@ -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),
|
||||
@@ -200,23 +200,5 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/infra_monitoring/checks", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.GetChecks),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetChecks",
|
||||
Tags: []string{"inframonitoring"},
|
||||
Summary: "Run Infra Monitoring Setup Checks",
|
||||
Description: "Checks whether the metrics and attributes required to power the infra-monitoring section selected by the 'type' query parameter (hosts, processes, pods, nodes, deployments, daemonsets, statefulsets, jobs, namespaces, clusters, volumes) are being received. For each collector receiver or processor that contributes required metrics or attributes, lists what is present and what is missing, with a prebuilt user-facing message and a docs link per missing component. Default-enabled metrics are those expected as soon as the receiver is configured; optional metrics require 'enabled: true' in receiver config. 'ready' is true only when every missing list is empty.",
|
||||
RequestQuery: new(inframonitoringtypes.PostableChecks),
|
||||
Response: new(inframonitoringtypes.Checks),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
156
pkg/apiserver/signozapiserver/metricreductionrule.go
Normal file
156
pkg/apiserver/signozapiserver/metricreductionrule.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addMetricReductionRuleRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/metric_reduction_rules", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricReductionRuleHandler.List),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListMetricReductionRules",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "List metric reduction rules",
|
||||
Description: "Returns active metric volume-control (label reduction) rules.",
|
||||
RequestQuery: new(metricreductionruletypes.ListReductionRulesParams),
|
||||
Response: new(metricreductionruletypes.GettableReductionRules),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusUnauthorized, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons, http.StatusInternalServerError},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metric_reduction_rules", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.metricReductionRuleHandler.Create),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateMetricReductionRule",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Create a metric reduction rule",
|
||||
Description: "Creates a volume-control rule for a metric and returns it with its id; fails if the metric already has a rule.",
|
||||
Request: new(metricreductionruletypes.PostableReductionRule),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(metricreductionruletypes.GettableReductionRule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusConflict, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons, http.StatusInternalServerError},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metric_reduction_rules/stats", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricReductionRuleHandler.Stats),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricReductionRuleStats",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Metric reduction stats",
|
||||
Description: "Returns total ingested vs retained series and the estimated monthly savings across all volume-control rules.",
|
||||
Response: new(metricreductionruletypes.GettableReductionRuleStats),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusUnauthorized, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons, http.StatusInternalServerError},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metric_reduction_rules/timeseries", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricReductionRuleHandler.Timeseries),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricReductionRuleTimeseries",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Metric reduction volume over time",
|
||||
Description: "Returns ingested vs retained series over time across all volume-control rules (hourly buckets), in the query-range time-series response shape.",
|
||||
Response: new(querybuildertypesv5.QueryRangeResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusUnauthorized, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons, http.StatusInternalServerError},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metric_reduction_rules/preview", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.metricReductionRuleHandler.Preview),
|
||||
handler.OpenAPIDef{
|
||||
ID: "PreviewMetricReductionRule",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Preview a metric reduction rule",
|
||||
Description: "Estimates the series reduction and related-asset impact of a candidate volume-control rule without persisting it.",
|
||||
Request: new(metricreductionruletypes.PostableReductionRulePreview),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(metricreductionruletypes.GettableReductionRulePreview),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons, http.StatusInternalServerError},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metric_reduction_rules/{id}", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricReductionRuleHandler.GetByID),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricReductionRuleByID",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Get a metric reduction rule by id",
|
||||
Description: "Returns a single volume-control rule by its id.",
|
||||
Response: new(metricreductionruletypes.GettableReductionRule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusUnauthorized, http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons, http.StatusInternalServerError},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metric_reduction_rules/{id}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.metricReductionRuleHandler.UpdateByID),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateMetricReductionRuleByID",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Update a metric reduction rule by id",
|
||||
Description: "Updates the match type and labels of a volume-control rule by its id; the metric name is immutable.",
|
||||
Request: new(metricreductionruletypes.UpdatableReductionRule),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(metricreductionruletypes.GettableReductionRule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons, http.StatusInternalServerError},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metric_reduction_rules/{id}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.metricReductionRuleHandler.DeleteByID),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteMetricReductionRuleByID",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Delete a metric reduction rule by id",
|
||||
Description: "Deletes a volume-control rule by its id.",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons, http.StatusInternalServerError},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
@@ -39,39 +40,40 @@ import (
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
config apiserver.Config
|
||||
settings factory.ScopedProviderSettings
|
||||
router *mux.Router
|
||||
authzMiddleware *middleware.AuthZ
|
||||
authzService authz.AuthZ
|
||||
orgHandler organization.Handler
|
||||
userHandler user.Handler
|
||||
sessionHandler session.Handler
|
||||
authDomainHandler authdomain.Handler
|
||||
preferenceHandler preference.Handler
|
||||
globalHandler global.Handler
|
||||
promoteHandler promote.Handler
|
||||
flaggerHandler flagger.Handler
|
||||
dashboardModule dashboard.Module
|
||||
dashboardHandler dashboard.Handler
|
||||
metricsExplorerHandler metricsexplorer.Handler
|
||||
infraMonitoringHandler inframonitoring.Handler
|
||||
gatewayHandler gateway.Handler
|
||||
fieldsHandler fields.Handler
|
||||
authzHandler authz.Handler
|
||||
rawDataExportHandler rawdataexport.Handler
|
||||
zeusHandler zeus.Handler
|
||||
querierHandler querier.Handler
|
||||
serviceAccountHandler serviceaccount.Handler
|
||||
factoryHandler factory.Handler
|
||||
cloudIntegrationHandler cloudintegration.Handler
|
||||
ruleStateHistoryHandler rulestatehistory.Handler
|
||||
spanMapperHandler spanmapper.Handler
|
||||
alertmanagerHandler alertmanager.Handler
|
||||
traceDetailHandler tracedetail.Handler
|
||||
rulerHandler ruler.Handler
|
||||
llmPricingRuleHandler llmpricingrule.Handler
|
||||
statsHandler statsreporter.Handler
|
||||
config apiserver.Config
|
||||
settings factory.ScopedProviderSettings
|
||||
router *mux.Router
|
||||
authzMiddleware *middleware.AuthZ
|
||||
authzService authz.AuthZ
|
||||
orgHandler organization.Handler
|
||||
userHandler user.Handler
|
||||
sessionHandler session.Handler
|
||||
authDomainHandler authdomain.Handler
|
||||
preferenceHandler preference.Handler
|
||||
globalHandler global.Handler
|
||||
promoteHandler promote.Handler
|
||||
flaggerHandler flagger.Handler
|
||||
dashboardModule dashboard.Module
|
||||
dashboardHandler dashboard.Handler
|
||||
metricsExplorerHandler metricsexplorer.Handler
|
||||
metricReductionRuleHandler metricreductionrule.Handler
|
||||
infraMonitoringHandler inframonitoring.Handler
|
||||
gatewayHandler gateway.Handler
|
||||
fieldsHandler fields.Handler
|
||||
authzHandler authz.Handler
|
||||
rawDataExportHandler rawdataexport.Handler
|
||||
zeusHandler zeus.Handler
|
||||
querierHandler querier.Handler
|
||||
serviceAccountHandler serviceaccount.Handler
|
||||
factoryHandler factory.Handler
|
||||
cloudIntegrationHandler cloudintegration.Handler
|
||||
ruleStateHistoryHandler rulestatehistory.Handler
|
||||
spanMapperHandler spanmapper.Handler
|
||||
alertmanagerHandler alertmanager.Handler
|
||||
traceDetailHandler tracedetail.Handler
|
||||
rulerHandler ruler.Handler
|
||||
llmPricingRuleHandler llmpricingrule.Handler
|
||||
statsHandler statsreporter.Handler
|
||||
}
|
||||
|
||||
func NewFactory(
|
||||
@@ -88,6 +90,7 @@ func NewFactory(
|
||||
dashboardModule dashboard.Module,
|
||||
dashboardHandler dashboard.Handler,
|
||||
metricsExplorerHandler metricsexplorer.Handler,
|
||||
metricReductionRuleHandler metricreductionrule.Handler,
|
||||
infraMonitoringHandler inframonitoring.Handler,
|
||||
gatewayHandler gateway.Handler,
|
||||
fieldsHandler fields.Handler,
|
||||
@@ -124,6 +127,7 @@ func NewFactory(
|
||||
dashboardModule,
|
||||
dashboardHandler,
|
||||
metricsExplorerHandler,
|
||||
metricReductionRuleHandler,
|
||||
infraMonitoringHandler,
|
||||
gatewayHandler,
|
||||
fieldsHandler,
|
||||
@@ -162,6 +166,7 @@ func newProvider(
|
||||
dashboardModule dashboard.Module,
|
||||
dashboardHandler dashboard.Handler,
|
||||
metricsExplorerHandler metricsexplorer.Handler,
|
||||
metricReductionRuleHandler metricreductionrule.Handler,
|
||||
infraMonitoringHandler inframonitoring.Handler,
|
||||
gatewayHandler gateway.Handler,
|
||||
fieldsHandler fields.Handler,
|
||||
@@ -184,38 +189,39 @@ func newProvider(
|
||||
router := mux.NewRouter().UseEncodedPath()
|
||||
|
||||
provider := &provider{
|
||||
config: config,
|
||||
settings: settings,
|
||||
router: router,
|
||||
orgHandler: orgHandler,
|
||||
userHandler: userHandler,
|
||||
authzService: authzService,
|
||||
sessionHandler: sessionHandler,
|
||||
authDomainHandler: authDomainHandler,
|
||||
preferenceHandler: preferenceHandler,
|
||||
globalHandler: globalHandler,
|
||||
promoteHandler: promoteHandler,
|
||||
flaggerHandler: flaggerHandler,
|
||||
dashboardModule: dashboardModule,
|
||||
dashboardHandler: dashboardHandler,
|
||||
metricsExplorerHandler: metricsExplorerHandler,
|
||||
infraMonitoringHandler: infraMonitoringHandler,
|
||||
gatewayHandler: gatewayHandler,
|
||||
fieldsHandler: fieldsHandler,
|
||||
authzHandler: authzHandler,
|
||||
rawDataExportHandler: rawDataExportHandler,
|
||||
zeusHandler: zeusHandler,
|
||||
querierHandler: querierHandler,
|
||||
serviceAccountHandler: serviceAccountHandler,
|
||||
factoryHandler: factoryHandler,
|
||||
cloudIntegrationHandler: cloudIntegrationHandler,
|
||||
ruleStateHistoryHandler: ruleStateHistoryHandler,
|
||||
spanMapperHandler: spanMapperHandler,
|
||||
alertmanagerHandler: alertmanagerHandler,
|
||||
traceDetailHandler: traceDetailHandler,
|
||||
rulerHandler: rulerHandler,
|
||||
llmPricingRuleHandler: llmPricingRuleHandler,
|
||||
statsHandler: statsHandler,
|
||||
config: config,
|
||||
settings: settings,
|
||||
router: router,
|
||||
orgHandler: orgHandler,
|
||||
userHandler: userHandler,
|
||||
authzService: authzService,
|
||||
sessionHandler: sessionHandler,
|
||||
authDomainHandler: authDomainHandler,
|
||||
preferenceHandler: preferenceHandler,
|
||||
globalHandler: globalHandler,
|
||||
promoteHandler: promoteHandler,
|
||||
flaggerHandler: flaggerHandler,
|
||||
dashboardModule: dashboardModule,
|
||||
dashboardHandler: dashboardHandler,
|
||||
metricsExplorerHandler: metricsExplorerHandler,
|
||||
metricReductionRuleHandler: metricReductionRuleHandler,
|
||||
infraMonitoringHandler: infraMonitoringHandler,
|
||||
gatewayHandler: gatewayHandler,
|
||||
fieldsHandler: fieldsHandler,
|
||||
authzHandler: authzHandler,
|
||||
rawDataExportHandler: rawDataExportHandler,
|
||||
zeusHandler: zeusHandler,
|
||||
querierHandler: querierHandler,
|
||||
serviceAccountHandler: serviceAccountHandler,
|
||||
factoryHandler: factoryHandler,
|
||||
cloudIntegrationHandler: cloudIntegrationHandler,
|
||||
ruleStateHistoryHandler: ruleStateHistoryHandler,
|
||||
spanMapperHandler: spanMapperHandler,
|
||||
alertmanagerHandler: alertmanagerHandler,
|
||||
traceDetailHandler: traceDetailHandler,
|
||||
rulerHandler: rulerHandler,
|
||||
llmPricingRuleHandler: llmPricingRuleHandler,
|
||||
statsHandler: statsHandler,
|
||||
}
|
||||
|
||||
provider.authzMiddleware = middleware.NewAuthZ(settings.Logger(), orgGetter, authzService)
|
||||
@@ -272,6 +278,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addMetricReductionRuleRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addInfraMonitoringRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
)
|
||||
|
||||
// splitBucket partitions one component bucket's metric and attribute lists
|
||||
// against the module-wide missing sets into up to six response entries.
|
||||
// Empty partitions are left nil so callers can skip them.
|
||||
func splitBucket(b checkComponentBucket, missingMetrics, missingAttrs map[string]bool) bucketSplit {
|
||||
var s bucketSplit
|
||||
presentDef, missDef := partitionList(b.DefaultMetrics, missingMetrics)
|
||||
if len(presentDef) > 0 {
|
||||
s.PresentDefault = &inframonitoringtypes.MetricsComponentEntry{
|
||||
Metrics: presentDef,
|
||||
AssociatedComponent: b.Component,
|
||||
}
|
||||
}
|
||||
if len(missDef) > 0 {
|
||||
s.MissingDefault = &inframonitoringtypes.MissingMetricsComponentEntry{
|
||||
MetricsComponentEntry: inframonitoringtypes.MetricsComponentEntry{
|
||||
Metrics: missDef,
|
||||
AssociatedComponent: b.Component,
|
||||
},
|
||||
Message: buildMissingDefaultMetricsMessage(missDef, b.Component.Name),
|
||||
DocumentationLink: b.DocumentationLink,
|
||||
}
|
||||
}
|
||||
|
||||
presentOpt, missOpt := partitionList(b.OptionalMetrics, missingMetrics)
|
||||
if len(presentOpt) > 0 {
|
||||
s.PresentOptional = &inframonitoringtypes.MetricsComponentEntry{
|
||||
Metrics: presentOpt,
|
||||
AssociatedComponent: b.Component,
|
||||
}
|
||||
}
|
||||
if len(missOpt) > 0 {
|
||||
s.MissingOptional = &inframonitoringtypes.MissingMetricsComponentEntry{
|
||||
MetricsComponentEntry: inframonitoringtypes.MetricsComponentEntry{
|
||||
Metrics: missOpt,
|
||||
AssociatedComponent: b.Component,
|
||||
},
|
||||
Message: buildMissingOptionalMetricsMessage(missOpt, b.Component.Name),
|
||||
DocumentationLink: b.DocumentationLink,
|
||||
}
|
||||
}
|
||||
|
||||
presentA, missA := partitionList(b.RequiredAttrs, missingAttrs)
|
||||
if len(presentA) > 0 {
|
||||
s.PresentAttrs = &inframonitoringtypes.AttributesComponentEntry{
|
||||
Attributes: presentA,
|
||||
AssociatedComponent: b.Component,
|
||||
}
|
||||
}
|
||||
if len(missA) > 0 {
|
||||
s.MissingAttrs = &inframonitoringtypes.MissingAttributesComponentEntry{
|
||||
AttributesComponentEntry: inframonitoringtypes.AttributesComponentEntry{
|
||||
Attributes: missA,
|
||||
AssociatedComponent: b.Component,
|
||||
},
|
||||
Message: buildMissingRequiredAttrsMessage(missA, b.Component.Name),
|
||||
DocumentationLink: b.DocumentationLink,
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// getSpecForType returns the checkSpec for a given CheckType, or an error if the type is invalid.
|
||||
func getSpecForType(t inframonitoringtypes.CheckType) (*checkSpec, error) {
|
||||
spec, ok := checkSpecs[t]
|
||||
if !ok {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "no checks spec for type: %s", t)
|
||||
}
|
||||
return &spec, nil
|
||||
}
|
||||
|
||||
// partitionList splits items into those NOT in `missing` and those in `missing`.
|
||||
// Preserves input order.
|
||||
func partitionList(items []string, missing map[string]bool) (present, miss []string) {
|
||||
for _, x := range items {
|
||||
if missing[x] {
|
||||
miss = append(miss, x)
|
||||
} else {
|
||||
present = append(present, x)
|
||||
}
|
||||
}
|
||||
return present, miss
|
||||
}
|
||||
|
||||
func buildMissingDefaultMetricsMessage(metrics []string, componentName string) string {
|
||||
return fmt.Sprintf(
|
||||
"Missing default metrics %s from %s. Learn how to configure here.",
|
||||
strings.Join(metrics, ", "), componentName,
|
||||
)
|
||||
}
|
||||
|
||||
func buildMissingOptionalMetricsMessage(metrics []string, componentName string) string {
|
||||
return fmt.Sprintf(
|
||||
"Missing optional metrics %s from %s. Learn how to enable here.",
|
||||
strings.Join(metrics, ", "), componentName,
|
||||
)
|
||||
}
|
||||
|
||||
func buildMissingRequiredAttrsMessage(attrs []string, componentName string) string {
|
||||
return fmt.Sprintf(
|
||||
"Missing required attributes %s from %s. Learn how to configure here.",
|
||||
strings.Join(attrs, ", "), componentName,
|
||||
)
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
package implinframonitoring
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
|
||||
// Component names — the 5 OTel collector receivers/processors that produce
|
||||
// metrics and resource attributes consumed by infra-monitoring tabs. Bare
|
||||
// strings on purpose (not wrapped enums) — the list is open-ended enough that
|
||||
// an enum adds more friction than value.
|
||||
const (
|
||||
componentNameHostMetricsReceiver = "hostmetricsreceiver"
|
||||
componentNameKubeletStatsReceiver = "kubeletstatsreceiver"
|
||||
componentNameK8sClusterReceiver = "k8sclusterreceiver"
|
||||
componentNameResourceDetectionProcessor = "resourcedetectionprocessor"
|
||||
componentNameK8sAttributesProcessor = "k8sattributesprocessor"
|
||||
)
|
||||
|
||||
// Documentation links — one per component. User-facing; emitted on missing-entries.
|
||||
const (
|
||||
docLinkHostMetricsReceiver = "https://signoz.io/docs/infrastructure-monitoring/user-guides/hostmetrics/#configure-the-hostmetrics-receiver"
|
||||
docLinkKubeletStatsReceiver = "https://signoz.io/docs/infrastructure-monitoring/user-guides/k8s-metrics/#setup-kubelet-stats-receiver"
|
||||
docLinkK8sClusterReceiver = "https://signoz.io/docs/infrastructure-monitoring/user-guides/k8s-metrics/#setup-k8s-cluster-receiver"
|
||||
docLinkResourceDetectionProcessor = "https://signoz.io/docs/infrastructure-monitoring/user-guides/hostmetrics/#configure-the-resourcedetection-processor"
|
||||
docLinkK8sAttributesProcessor = "https://signoz.io/docs/infrastructure-monitoring/user-guides/k8s-metrics/#3-setup-k8sattributesprocessor-to-enable-kubernetes-metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
componentHostMetricsReceiver = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.CheckComponentTypeReceiver,
|
||||
Name: componentNameHostMetricsReceiver,
|
||||
}
|
||||
componentKubeletStatsReceiver = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.CheckComponentTypeReceiver,
|
||||
Name: componentNameKubeletStatsReceiver,
|
||||
}
|
||||
componentK8sClusterReceiver = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.CheckComponentTypeReceiver,
|
||||
Name: componentNameK8sClusterReceiver,
|
||||
}
|
||||
componentResourceDetectionProcessor = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.CheckComponentTypeProcessor,
|
||||
Name: componentNameResourceDetectionProcessor,
|
||||
}
|
||||
componentK8sAttributesProcessor = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.CheckComponentTypeProcessor,
|
||||
Name: componentNameK8sAttributesProcessor,
|
||||
}
|
||||
)
|
||||
|
||||
// checkSpecs is the single lookup table the module consults for a type's
|
||||
// readiness contract. Every CheckType value must have an entry here.
|
||||
var checkSpecs = map[inframonitoringtypes.CheckType]checkSpec{
|
||||
inframonitoringtypes.CheckTypeHosts: hostsSpec,
|
||||
inframonitoringtypes.CheckTypeProcesses: processesSpec,
|
||||
inframonitoringtypes.CheckTypePods: podsSpec,
|
||||
inframonitoringtypes.CheckTypeNodes: nodesSpec,
|
||||
inframonitoringtypes.CheckTypeDeployments: deploymentsSpec,
|
||||
inframonitoringtypes.CheckTypeDaemonsets: daemonsetsSpec,
|
||||
inframonitoringtypes.CheckTypeStatefulsets: statefulsetsSpec,
|
||||
inframonitoringtypes.CheckTypeJobs: jobsSpec,
|
||||
inframonitoringtypes.CheckTypeNamespaces: namespacesSpec,
|
||||
inframonitoringtypes.CheckTypeClusters: clustersSpec,
|
||||
inframonitoringtypes.CheckTypeVolumes: volumesSpec,
|
||||
}
|
||||
|
||||
// Per-type specs. Every metric and attribute is spelled out in its own spec
|
||||
// on purpose — no shared slices, no concatenation helpers. Repetition is
|
||||
// cheaper than indirection when auditing what each tab actually requires.
|
||||
|
||||
var hostsSpec = checkSpec{
|
||||
Buckets: []checkComponentBucket{
|
||||
{
|
||||
Component: componentHostMetricsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"system.cpu.time",
|
||||
"system.memory.usage",
|
||||
"system.cpu.load_average.15m",
|
||||
"system.filesystem.usage",
|
||||
},
|
||||
DocumentationLink: docLinkHostMetricsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"host.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var processesSpec = checkSpec{
|
||||
Buckets: []checkComponentBucket{
|
||||
{
|
||||
Component: componentHostMetricsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"process.cpu.time",
|
||||
"process.memory.usage",
|
||||
},
|
||||
RequiredAttrs: []string{"process.pid"},
|
||||
DocumentationLink: docLinkHostMetricsReceiver,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var podsSpec = checkSpec{
|
||||
Buckets: []checkComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{"k8s.pod.phase"},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.pod.uid"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var nodesSpec = checkSpec{
|
||||
Buckets: []checkComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.node.cpu.usage",
|
||||
"k8s.node.memory.working_set",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.node.allocatable_cpu",
|
||||
"k8s.node.allocatable_memory", // k8s.node.allocatable_cpu and k8s.node.allocatable_memory are
|
||||
// controlled by allocatable_types_to_report config option (Check // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/4f9a578b210a6dcb9f9bf47942f27208b5765298/receiver/k8sclusterreceiver/metadata.yaml#L805-L806)
|
||||
"k8s.node.condition_ready", // # k8s.node.condition_* metrics (k8s.node.condition_ready, k8s.node.condition_memory_pressure, etc) are controlled# by node_conditions_to_report config option.
|
||||
// By default, only k8s.node.condition_ready is enabled. (Check https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/4f9a578b210a6dcb9f9bf47942f27208b5765298/receiver/k8sclusterreceiver/metadata.yaml#L802)
|
||||
"k8s.pod.phase", // pod counts per node by phase
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.node.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var deploymentsSpec = checkSpec{
|
||||
Buckets: []checkComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.phase",
|
||||
"k8s.deployment.desired",
|
||||
"k8s.deployment.available",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.deployment.name", "k8s.namespace.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var daemonsetsSpec = checkSpec{
|
||||
Buckets: []checkComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.phase",
|
||||
"k8s.daemonset.desired_scheduled_nodes",
|
||||
"k8s.daemonset.current_scheduled_nodes",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.daemonset.name", "k8s.namespace.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var statefulsetsSpec = checkSpec{
|
||||
Buckets: []checkComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.phase",
|
||||
"k8s.statefulset.desired_pods",
|
||||
"k8s.statefulset.current_pods",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.statefulset.name", "k8s.namespace.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var jobsSpec = checkSpec{
|
||||
Buckets: []checkComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.phase",
|
||||
"k8s.job.desired_successful_pods",
|
||||
"k8s.job.active_pods",
|
||||
"k8s.job.failed_pods",
|
||||
"k8s.job.successful_pods",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.job.name", "k8s.namespace.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var namespacesSpec = checkSpec{
|
||||
Buckets: []checkComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{"k8s.pod.phase"},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.namespace.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var clustersSpec = checkSpec{
|
||||
Buckets: []checkComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.node.cpu.usage",
|
||||
"k8s.node.memory.working_set",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.node.allocatable_cpu",
|
||||
"k8s.node.allocatable_memory", //k8s.node.allocatable_cpu and k8s.node.allocatable_memory are
|
||||
// controlled by allocatable_types_to_report config option (Check // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/4f9a578b210a6dcb9f9bf47942f27208b5765298/receiver/k8sclusterreceiver/metadata.yaml#L805-L806)
|
||||
"k8s.node.condition_ready", // node counts by readiness
|
||||
"k8s.pod.phase", // pod counts per cluster by phase
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var volumesSpec = checkSpec{
|
||||
Buckets: []checkComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.volume.available",
|
||||
"k8s.volume.capacity",
|
||||
"k8s.volume.inodes",
|
||||
"k8s.volume.inodes.free",
|
||||
"k8s.volume.inodes.used",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.persistentvolumeclaim.name", "k8s.namespace.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Component used across splitBucket cases — it's a processor so the test
|
||||
// doesn't carry any receiver semantics.
|
||||
var testComponent = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.CheckComponentTypeReceiver,
|
||||
Name: "testreceiver",
|
||||
}
|
||||
|
||||
const testDocLink = "https://example.com/docs"
|
||||
|
||||
func TestSplitBucket(t *testing.T) {
|
||||
type want struct {
|
||||
presentDefault []string
|
||||
presentOptional []string
|
||||
presentAttrs []string
|
||||
missingDefault []string
|
||||
missingOptional []string
|
||||
missingAttrs []string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
bucket checkComponentBucket
|
||||
missingMetrics map[string]bool
|
||||
missingAttrs map[string]bool
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "empty bucket — nothing to emit",
|
||||
bucket: checkComponentBucket{Component: testComponent, DocumentationLink: testDocLink},
|
||||
missingMetrics: map[string]bool{},
|
||||
missingAttrs: map[string]bool{},
|
||||
want: want{},
|
||||
},
|
||||
{
|
||||
name: "all default metrics present",
|
||||
bucket: checkComponentBucket{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"m1", "m2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{},
|
||||
missingAttrs: map[string]bool{},
|
||||
want: want{
|
||||
presentDefault: []string{"m1", "m2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all default metrics missing",
|
||||
bucket: checkComponentBucket{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"m1", "m2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{"m1": true, "m2": true},
|
||||
missingAttrs: map[string]bool{},
|
||||
want: want{
|
||||
missingDefault: []string{"m1", "m2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed default metrics",
|
||||
bucket: checkComponentBucket{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"m1", "m2", "m3"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{"m2": true},
|
||||
missingAttrs: map[string]bool{},
|
||||
want: want{
|
||||
presentDefault: []string{"m1", "m3"},
|
||||
missingDefault: []string{"m2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only optional metrics — all missing",
|
||||
bucket: checkComponentBucket{
|
||||
Component: testComponent,
|
||||
OptionalMetrics: []string{"opt1", "opt2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{"opt1": true, "opt2": true},
|
||||
missingAttrs: map[string]bool{},
|
||||
want: want{
|
||||
missingOptional: []string{"opt1", "opt2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only required attrs — all present",
|
||||
bucket: checkComponentBucket{
|
||||
Component: testComponent,
|
||||
RequiredAttrs: []string{"a1", "a2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{},
|
||||
missingAttrs: map[string]bool{},
|
||||
want: want{
|
||||
presentAttrs: []string{"a1", "a2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only required attrs — all missing",
|
||||
bucket: checkComponentBucket{
|
||||
Component: testComponent,
|
||||
RequiredAttrs: []string{"a1"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{},
|
||||
missingAttrs: map[string]bool{"a1": true},
|
||||
want: want{
|
||||
missingAttrs: []string{"a1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "every dimension populated on both sides",
|
||||
bucket: checkComponentBucket{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"d1", "d2"},
|
||||
OptionalMetrics: []string{"o1", "o2"},
|
||||
RequiredAttrs: []string{"a1", "a2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{"d2": true, "o1": true},
|
||||
missingAttrs: map[string]bool{"a2": true},
|
||||
want: want{
|
||||
presentDefault: []string{"d1"},
|
||||
missingDefault: []string{"d2"},
|
||||
presentOptional: []string{"o2"},
|
||||
missingOptional: []string{"o1"},
|
||||
presentAttrs: []string{"a1"},
|
||||
missingAttrs: []string{"a2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := splitBucket(tt.bucket, tt.missingMetrics, tt.missingAttrs)
|
||||
|
||||
requireMetricsEntry(t, "presentDefault", got.PresentDefault, tt.want.presentDefault)
|
||||
requireMetricsEntry(t, "presentOptional", got.PresentOptional, tt.want.presentOptional)
|
||||
requireAttrsEntry(t, "presentAttrs", got.PresentAttrs, tt.want.presentAttrs)
|
||||
|
||||
requireMissingMetrics(t, "missingDefault", got.MissingDefault, tt.want.missingDefault)
|
||||
requireMissingMetrics(t, "missingOptional", got.MissingOptional, tt.want.missingOptional)
|
||||
requireMissingAttrs(t, "missingAttrs", got.MissingAttrs, tt.want.missingAttrs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartitionList(t *testing.T) {
|
||||
present, missing := partitionList(
|
||||
[]string{"a", "b", "c", "d"},
|
||||
map[string]bool{"b": true, "d": true},
|
||||
)
|
||||
require.Equal(t, []string{"a", "c"}, present)
|
||||
require.Equal(t, []string{"b", "d"}, missing)
|
||||
}
|
||||
|
||||
func TestMissingMessageTemplates(t *testing.T) {
|
||||
require.Equal(t,
|
||||
"Missing default metrics m1, m2 from comp. Learn how to configure here.",
|
||||
buildMissingDefaultMetricsMessage([]string{"m1", "m2"}, "comp"),
|
||||
)
|
||||
require.Equal(t,
|
||||
"Missing optional metrics m1 from comp. Learn how to enable here.",
|
||||
buildMissingOptionalMetricsMessage([]string{"m1"}, "comp"),
|
||||
)
|
||||
require.Equal(t,
|
||||
"Missing required attributes a1 from comp. Learn how to configure here.",
|
||||
buildMissingRequiredAttrsMessage([]string{"a1"}, "comp"),
|
||||
)
|
||||
require.Equal(t,
|
||||
"Missing required attributes a1, a2 from comp. Learn how to configure here.",
|
||||
buildMissingRequiredAttrsMessage([]string{"a1", "a2"}, "comp"),
|
||||
)
|
||||
}
|
||||
|
||||
// TestChecksSpecs_CoverAllTypes ensures the spec map has an entry for
|
||||
// every CheckType — prevents silently shipping an checks type that
|
||||
// has no spec and would 500 at runtime.
|
||||
func TestChecksSpecs_CoverAllTypes(t *testing.T) {
|
||||
for _, tp := range inframonitoringtypes.ValidCheckTypes {
|
||||
_, ok := checkSpecs[tp]
|
||||
require.True(t, ok, "missing checks spec for type %s", tp)
|
||||
}
|
||||
require.Len(t, checkSpecs, len(inframonitoringtypes.ValidCheckTypes))
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func requireMetricsEntry(t *testing.T, name string, got *inframonitoringtypes.MetricsComponentEntry, wantMetrics []string) {
|
||||
t.Helper()
|
||||
if len(wantMetrics) == 0 {
|
||||
require.Nil(t, got, name)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got, name)
|
||||
require.Equal(t, wantMetrics, got.Metrics, name)
|
||||
require.Equal(t, testComponent, got.AssociatedComponent, name)
|
||||
}
|
||||
|
||||
func requireAttrsEntry(t *testing.T, name string, got *inframonitoringtypes.AttributesComponentEntry, wantAttrs []string) {
|
||||
t.Helper()
|
||||
if len(wantAttrs) == 0 {
|
||||
require.Nil(t, got, name)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got, name)
|
||||
require.Equal(t, wantAttrs, got.Attributes, name)
|
||||
require.Equal(t, testComponent, got.AssociatedComponent, name)
|
||||
}
|
||||
|
||||
func requireMissingMetrics(t *testing.T, name string, got *inframonitoringtypes.MissingMetricsComponentEntry, wantMetrics []string) {
|
||||
t.Helper()
|
||||
if len(wantMetrics) == 0 {
|
||||
require.Nil(t, got, name)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got, name)
|
||||
require.Equal(t, wantMetrics, got.Metrics, name)
|
||||
require.Equal(t, testComponent, got.AssociatedComponent, name)
|
||||
require.NotEmpty(t, got.Message, name)
|
||||
require.Equal(t, testDocLink, got.DocumentationLink, name)
|
||||
}
|
||||
|
||||
func requireMissingAttrs(t *testing.T, name string, got *inframonitoringtypes.MissingAttributesComponentEntry, wantAttrs []string) {
|
||||
t.Helper()
|
||||
if len(wantAttrs) == 0 {
|
||||
require.Nil(t, got, name)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got, name)
|
||||
require.Equal(t, wantAttrs, got.Attributes, name)
|
||||
require.Equal(t, testComponent, got.AssociatedComponent, name)
|
||||
require.NotEmpty(t, got.Message, name)
|
||||
require.Equal(t, testDocLink, got.DocumentationLink, name)
|
||||
}
|
||||
@@ -22,30 +22,6 @@ func NewHandler(m inframonitoring.Module) inframonitoring.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) GetChecks(rw http.ResponseWriter, req *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(req.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
var parsedReq inframonitoringtypes.PostableChecks
|
||||
if err := binding.Query.BindQuery(req.URL.Query(), &parsedReq); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.module.GetChecks(req.Context(), orgID, &parsedReq)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *handler) ListHosts(rw http.ResponseWriter, req *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(req.Context())
|
||||
if err != nil {
|
||||
|
||||
@@ -413,42 +413,20 @@ 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.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.AttributesMetadataTableName))
|
||||
sb.Where(sb.In("metric_name", sqlbuilder.List(metricNames)))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return minFirstReported, nil
|
||||
}
|
||||
|
||||
// getMetricsExistence returns, for each requested metric name, whether it has ever
|
||||
// been reported (present in signoz_metrics.distributed_metadata). No time window.
|
||||
func (m *module) getMetricsExistence(ctx context.Context, metricNames []string) (map[string]bool, error) {
|
||||
present := make(map[string]bool, len(metricNames))
|
||||
for _, n := range metricNames {
|
||||
present[n] = false
|
||||
}
|
||||
if len(metricNames) == 0 {
|
||||
return present, nil
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("metric_name", "count(*) AS cnt")
|
||||
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")
|
||||
@@ -457,73 +435,42 @@ func (m *module) getMetricsExistence(ctx context.Context, metricNames []string)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var cnt uint64
|
||||
if err := rows.Scan(&name, &cnt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cnt > 0 {
|
||||
present[name] = true
|
||||
}
|
||||
type metricInfo struct {
|
||||
count uint64
|
||||
minFirstReported uint64
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return present, nil
|
||||
}
|
||||
|
||||
// getAttributesExistence returns, for each requested attrName, whether it has ever
|
||||
// been reported as a label on any of the given metricNames. Presence is checked
|
||||
// against distributed_metadata without a time-range filter.
|
||||
func (m *module) getAttributesExistence(ctx context.Context, metricNames, attrNames []string) (map[string]bool, error) {
|
||||
present := make(map[string]bool, len(attrNames))
|
||||
for _, a := range attrNames {
|
||||
present[a] = false
|
||||
}
|
||||
if len(attrNames) == 0 {
|
||||
return present, nil
|
||||
}
|
||||
if len(metricNames) == 0 {
|
||||
return nil, errors.NewInternalf(errors.CodeInternal, "getAttributesExistence: metricNames must not be empty")
|
||||
}
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("attr_name", "count(*) AS cnt")
|
||||
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.AttributesMetadataTableName))
|
||||
sb.Where(
|
||||
sb.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
sb.In("attr_name", sqlbuilder.List(attrNames)),
|
||||
)
|
||||
sb.GroupBy("attr_name")
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
found := make(map[string]metricInfo, len(metricNames))
|
||||
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var cnt uint64
|
||||
if err := rows.Scan(&name, &cnt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if name != "" && cnt > 0 {
|
||||
present[name] = true
|
||||
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, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return present, 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,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package implinframonitoring
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
|
||||
// The types in this file are only used within the implinframonitoring package, and are not exposed outside.
|
||||
// They are primarily used for internal processing and structuring of data within the module's implementation.
|
||||
|
||||
@@ -31,50 +29,3 @@ type nodeConditionCounts struct {
|
||||
Ready int
|
||||
NotReady int
|
||||
}
|
||||
|
||||
// bucketSplit carries the up-to-six entries a single spec bucket contributes
|
||||
// to an checks response. Any field may be nil if the bucket doesn't
|
||||
// populate that dimension.
|
||||
type bucketSplit struct {
|
||||
PresentDefault *inframonitoringtypes.MetricsComponentEntry
|
||||
PresentOptional *inframonitoringtypes.MetricsComponentEntry
|
||||
PresentAttrs *inframonitoringtypes.AttributesComponentEntry
|
||||
MissingDefault *inframonitoringtypes.MissingMetricsComponentEntry
|
||||
MissingOptional *inframonitoringtypes.MissingMetricsComponentEntry
|
||||
MissingAttrs *inframonitoringtypes.MissingAttributesComponentEntry
|
||||
}
|
||||
|
||||
// checkComponentBucket is a single collector component's contribution
|
||||
// toward a single infra-monitoring tab's readiness. Any of the three dimension
|
||||
// slices (DefaultMetrics, OptionalMetrics, RequiredAttrs) may be empty — the
|
||||
// bucketizer in Phase 4 skips empty dimensions.
|
||||
type checkComponentBucket struct {
|
||||
Component inframonitoringtypes.AssociatedComponent
|
||||
DefaultMetrics []string
|
||||
OptionalMetrics []string
|
||||
RequiredAttrs []string
|
||||
DocumentationLink string
|
||||
}
|
||||
|
||||
// checkSpec defines, for one CheckType, the full set of
|
||||
// component-scoped buckets that must be satisfied for the tab to be ready.
|
||||
type checkSpec struct {
|
||||
Buckets []checkComponentBucket
|
||||
}
|
||||
|
||||
func (s checkSpec) getAllMetrics() []string {
|
||||
var out []string
|
||||
for _, b := range s.Buckets {
|
||||
out = append(out, b.DefaultMetrics...)
|
||||
out = append(out, b.OptionalMetrics...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s checkSpec) getAllAttrs() []string {
|
||||
var out []string
|
||||
for _, b := range s.Buckets {
|
||||
out = append(out, b.RequiredAttrs...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -49,84 +49,6 @@ func NewModule(
|
||||
}
|
||||
}
|
||||
|
||||
// GetChecks runs a per-type readiness check: for the requested
|
||||
// infra-monitoring tab, reports which required metrics and attributes are
|
||||
// present vs missing, grouped by the collector component that produces them.
|
||||
// Ready is true iff every missing list is empty.
|
||||
func (m *module) GetChecks(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableChecks) (*inframonitoringtypes.Checks, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spec, err := getSpecForType(req.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allMetrics := spec.getAllMetrics()
|
||||
allAttrs := spec.getAllAttrs()
|
||||
|
||||
presentMetrics, err := m.getMetricsExistence(ctx, allMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
missingMetricsMap := make(map[string]bool, len(allMetrics))
|
||||
for _, name := range allMetrics {
|
||||
if !presentMetrics[name] {
|
||||
missingMetricsMap[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
presentAttrs, err := m.getAttributesExistence(ctx, allMetrics, allAttrs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
missingAttrsMap := make(map[string]bool, len(allAttrs))
|
||||
for _, name := range allAttrs {
|
||||
if !presentAttrs[name] {
|
||||
missingAttrsMap[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
resp := &inframonitoringtypes.Checks{
|
||||
Type: req.Type,
|
||||
PresentDefaultEnabledMetrics: []inframonitoringtypes.MetricsComponentEntry{},
|
||||
PresentOptionalMetrics: []inframonitoringtypes.MetricsComponentEntry{},
|
||||
PresentRequiredAttributes: []inframonitoringtypes.AttributesComponentEntry{},
|
||||
MissingDefaultEnabledMetrics: []inframonitoringtypes.MissingMetricsComponentEntry{},
|
||||
MissingOptionalMetrics: []inframonitoringtypes.MissingMetricsComponentEntry{},
|
||||
MissingRequiredAttributes: []inframonitoringtypes.MissingAttributesComponentEntry{},
|
||||
}
|
||||
|
||||
for _, b := range spec.Buckets {
|
||||
s := splitBucket(b, missingMetricsMap, missingAttrsMap)
|
||||
if s.PresentDefault != nil {
|
||||
resp.PresentDefaultEnabledMetrics = append(resp.PresentDefaultEnabledMetrics, *s.PresentDefault)
|
||||
}
|
||||
if s.PresentOptional != nil {
|
||||
resp.PresentOptionalMetrics = append(resp.PresentOptionalMetrics, *s.PresentOptional)
|
||||
}
|
||||
if s.PresentAttrs != nil {
|
||||
resp.PresentRequiredAttributes = append(resp.PresentRequiredAttributes, *s.PresentAttrs)
|
||||
}
|
||||
if s.MissingDefault != nil {
|
||||
resp.MissingDefaultEnabledMetrics = append(resp.MissingDefaultEnabledMetrics, *s.MissingDefault)
|
||||
}
|
||||
if s.MissingOptional != nil {
|
||||
resp.MissingOptionalMetrics = append(resp.MissingOptionalMetrics, *s.MissingOptional)
|
||||
}
|
||||
if s.MissingAttrs != nil {
|
||||
resp.MissingRequiredAttributes = append(resp.MissingRequiredAttributes, *s.MissingAttrs)
|
||||
}
|
||||
}
|
||||
|
||||
resp.Ready = len(resp.MissingDefaultEnabledMetrics) == 0 &&
|
||||
len(resp.MissingOptionalMetrics) == 0 &&
|
||||
len(resp.MissingRequiredAttributes) == 0
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableHosts) (*inframonitoringtypes.Hosts, error) {
|
||||
ctx = m.withInfraMonitoringContext(ctx, "ListHosts")
|
||||
|
||||
@@ -156,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.
|
||||
@@ -261,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 {
|
||||
@@ -339,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 {
|
||||
@@ -422,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 {
|
||||
@@ -499,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 {
|
||||
@@ -589,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 {
|
||||
@@ -667,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 {
|
||||
@@ -750,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 {
|
||||
@@ -835,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 {
|
||||
@@ -920,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 {
|
||||
|
||||
@@ -20,7 +20,6 @@ type Handler interface {
|
||||
ListStatefulSets(http.ResponseWriter, *http.Request)
|
||||
ListJobs(http.ResponseWriter, *http.Request)
|
||||
ListDaemonSets(http.ResponseWriter, *http.Request)
|
||||
GetChecks(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type Module interface {
|
||||
@@ -35,5 +34,4 @@ type Module interface {
|
||||
ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableStatefulSets) (*inframonitoringtypes.StatefulSets, error)
|
||||
ListJobs(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableJobs) (*inframonitoringtypes.Jobs, error)
|
||||
ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableDaemonSets) (*inframonitoringtypes.DaemonSets, error)
|
||||
GetChecks(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableChecks) (*inframonitoringtypes.Checks, error)
|
||||
}
|
||||
|
||||
@@ -6,22 +6,19 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/opamptypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type module struct {
|
||||
store llmpricingruletypes.Store
|
||||
flagger flagger.Flagger
|
||||
store llmpricingruletypes.Store
|
||||
}
|
||||
|
||||
func NewModule(store llmpricingruletypes.Store, flagger flagger.Flagger) llmpricingrule.Module {
|
||||
return &module{store: store, flagger: flagger}
|
||||
func NewModule(store llmpricingruletypes.Store) llmpricingrule.Module {
|
||||
return &module{store: store}
|
||||
}
|
||||
|
||||
func (module *module) List(ctx context.Context, orgID valuer.UUID, offset, limit int, search string, isOverride *bool) ([]*llmpricingruletypes.LLMPricingRule, int, error) {
|
||||
@@ -92,16 +89,6 @@ func (module *module) AgentFeatureType() agentConf.AgentFeatureType {
|
||||
func (module *module) RecommendAgentConfig(orgID valuer.UUID, currentConfYaml []byte, configVersion *opamptypes.AgentConfigVersion) ([]byte, string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Skip the llm pricing processor unless AI observability is enabled for the org.
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
enabled, err := module.flagger.Boolean(ctx, flagger.FeatureEnableAIObservability, evalCtx)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if !enabled {
|
||||
return currentConfYaml, "", nil
|
||||
}
|
||||
|
||||
rules, err := module.getEnabledRules(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
package implmetricreductionrule
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
module metricreductionrule.Module
|
||||
}
|
||||
|
||||
func NewHandler(module metricreductionrule.Module) metricreductionrule.Handler {
|
||||
return &handler{module: module}
|
||||
}
|
||||
|
||||
func idFromPath(r *http.Request) (valuer.UUID, error) {
|
||||
id, err := valuer.NewUUID(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
return valuer.UUID{}, errors.NewInvalidInputf(errors.CodeInvalidInput, "id must be a valid uuid")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (h *handler) List(rw http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var params metricreductionruletypes.ListReductionRulesParams
|
||||
if err := binding.Query.BindQuery(r.URL.Query(), ¶ms); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := h.module.List(r.Context(), valuer.MustNewUUID(claims.OrgID), ¶ms)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *handler) Preview(rw http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var in metricreductionruletypes.PostableReductionRulePreview
|
||||
if err := binding.JSON.BindBody(r.Body, &in); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := h.module.Preview(r.Context(), valuer.MustNewUUID(claims.OrgID), &in)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *handler) Stats(rw http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := h.module.Stats(r.Context(), valuer.MustNewUUID(claims.OrgID))
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *handler) Timeseries(rw http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := h.module.Timeseries(r.Context(), valuer.MustNewUUID(claims.OrgID))
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *handler) Create(rw http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var in metricreductionruletypes.PostableReductionRule
|
||||
if err := binding.JSON.BindBody(r.Body, &in); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
out, err := h.module.Create(r.Context(), valuer.MustNewUUID(claims.OrgID), claims.Email, &in)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, out)
|
||||
}
|
||||
|
||||
func (h *handler) GetByID(rw http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := idFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := h.module.GetByID(r.Context(), valuer.MustNewUUID(claims.OrgID), id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *handler) UpdateByID(rw http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := idFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var in metricreductionruletypes.UpdatableReductionRule
|
||||
if err := binding.JSON.BindBody(r.Body, &in); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := h.module.UpdateByID(r.Context(), valuer.MustNewUUID(claims.OrgID), claims.Email, id, &in)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *handler) DeleteByID(rw http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := idFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.module.DeleteByID(r.Context(), valuer.MustNewUUID(claims.OrgID), id); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package implmetricreductionrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
|
||||
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type module struct{}
|
||||
|
||||
func NewModule() metricreductionrule.Module {
|
||||
return &module{}
|
||||
}
|
||||
|
||||
var errUnsupported = errors.New(errors.TypeUnsupported, metricreductionruletypes.ErrCodeMetricReductionRuleUnsupported,
|
||||
"metric volume control is an enterprise feature")
|
||||
|
||||
func (m *module) List(_ context.Context, _ valuer.UUID, _ *metricreductionruletypes.ListReductionRulesParams) (*metricreductionruletypes.GettableReductionRules, error) {
|
||||
return nil, errUnsupported
|
||||
}
|
||||
|
||||
func (m *module) Create(_ context.Context, _ valuer.UUID, _ string, _ *metricreductionruletypes.PostableReductionRule) (*metricreductionruletypes.GettableReductionRule, error) {
|
||||
return nil, errUnsupported
|
||||
}
|
||||
|
||||
func (m *module) GetByID(_ context.Context, _ valuer.UUID, _ valuer.UUID) (*metricreductionruletypes.GettableReductionRule, error) {
|
||||
return nil, errUnsupported
|
||||
}
|
||||
|
||||
func (m *module) UpdateByID(_ context.Context, _ valuer.UUID, _ string, _ valuer.UUID, _ *metricreductionruletypes.UpdatableReductionRule) (*metricreductionruletypes.GettableReductionRule, error) {
|
||||
return nil, errUnsupported
|
||||
}
|
||||
|
||||
func (m *module) DeleteByID(_ context.Context, _ valuer.UUID, _ valuer.UUID) error {
|
||||
return errUnsupported
|
||||
}
|
||||
|
||||
func (m *module) Preview(_ context.Context, _ valuer.UUID, _ *metricreductionruletypes.PostableReductionRulePreview) (*metricreductionruletypes.GettableReductionRulePreview, error) {
|
||||
return nil, errUnsupported
|
||||
}
|
||||
|
||||
func (m *module) Stats(_ context.Context, _ valuer.UUID) (*metricreductionruletypes.GettableReductionRuleStats, error) {
|
||||
return nil, errUnsupported
|
||||
}
|
||||
|
||||
func (m *module) Timeseries(_ context.Context, _ valuer.UUID) (*querybuildertypesv5.QueryRangeResponse, error) {
|
||||
return nil, errUnsupported
|
||||
}
|
||||
32
pkg/modules/metricreductionrule/metricreductionrule.go
Normal file
32
pkg/modules/metricreductionrule/metricreductionrule.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package metricreductionrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
List(ctx context.Context, orgID valuer.UUID, params *metricreductionruletypes.ListReductionRulesParams) (*metricreductionruletypes.GettableReductionRules, error)
|
||||
Create(ctx context.Context, orgID valuer.UUID, userEmail string, req *metricreductionruletypes.PostableReductionRule) (*metricreductionruletypes.GettableReductionRule, error)
|
||||
GetByID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*metricreductionruletypes.GettableReductionRule, error)
|
||||
UpdateByID(ctx context.Context, orgID valuer.UUID, userEmail string, id valuer.UUID, req *metricreductionruletypes.UpdatableReductionRule) (*metricreductionruletypes.GettableReductionRule, error)
|
||||
DeleteByID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||
Preview(ctx context.Context, orgID valuer.UUID, req *metricreductionruletypes.PostableReductionRulePreview) (*metricreductionruletypes.GettableReductionRulePreview, error)
|
||||
Stats(ctx context.Context, orgID valuer.UUID) (*metricreductionruletypes.GettableReductionRuleStats, error)
|
||||
Timeseries(ctx context.Context, orgID valuer.UUID) (*querybuildertypesv5.QueryRangeResponse, error)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
List(rw http.ResponseWriter, r *http.Request)
|
||||
Create(rw http.ResponseWriter, r *http.Request)
|
||||
GetByID(rw http.ResponseWriter, r *http.Request)
|
||||
UpdateByID(rw http.ResponseWriter, r *http.Request)
|
||||
DeleteByID(rw http.ResponseWriter, r *http.Request)
|
||||
Preview(rw http.ResponseWriter, r *http.Request)
|
||||
Stats(rw http.ResponseWriter, r *http.Request)
|
||||
Timeseries(rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
@@ -4,22 +4,19 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/opamptypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type module struct {
|
||||
store spantypes.SpanMapperStore
|
||||
flagger flagger.Flagger
|
||||
store spantypes.SpanMapperStore
|
||||
}
|
||||
|
||||
func NewModule(store spantypes.SpanMapperStore, flagger flagger.Flagger) spanmapper.Module {
|
||||
return &module{store: store, flagger: flagger}
|
||||
func NewModule(store spantypes.SpanMapperStore) spanmapper.Module {
|
||||
return &module{store: store}
|
||||
}
|
||||
|
||||
func (module *module) ListGroups(ctx context.Context, orgID valuer.UUID, q *spantypes.ListSpanMapperGroupsQuery) ([]*spantypes.SpanMapperGroup, error) {
|
||||
@@ -112,22 +109,12 @@ func (module *module) AgentFeatureType() agentConf.AgentFeatureType {
|
||||
func (module *module) RecommendAgentConfig(orgID valuer.UUID, currentConfYaml []byte, configVersion *opamptypes.AgentConfigVersion) ([]byte, string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Skip the llm pricing processor unless AI observability is enabled for the org.
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
enabled, err := module.flagger.Boolean(ctx, flagger.FeatureEnableAIObservability, evalCtx)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if !enabled {
|
||||
return currentConfYaml, "", nil
|
||||
}
|
||||
|
||||
enabledMappers, err := module.listEnabledGroupsWithMappers(ctx, orgID)
|
||||
enabled, err := module.listEnabledGroupsWithMappers(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
updatedConf, err := spantypes.GenerateCollectorConfigWithSpanMapperProcessor(currentConfYaml, enabledMappers)
|
||||
updatedConf, err := spantypes.GenerateCollectorConfigWithSpanMapperProcessor(currentConfYaml, enabled)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
@@ -113,8 +113,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
Store: signoz.SQLStore,
|
||||
AgentFeatures: []agentConf.AgentFeature{
|
||||
logParsingPipelineController,
|
||||
signoz.Modules.SpanMapper,
|
||||
signoz.Modules.LLMPricingRule,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -24,6 +24,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring/implinframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule/impllmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule/implmetricreductionrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
@@ -64,6 +66,7 @@ type Handlers struct {
|
||||
SpanPercentile spanpercentile.Handler
|
||||
Services services.Handler
|
||||
MetricsExplorer metricsexplorer.Handler
|
||||
MetricReductionRule metricreductionrule.Handler
|
||||
InfraMonitoring inframonitoring.Handler
|
||||
Global global.Handler
|
||||
FlaggerHandler flagger.Handler
|
||||
@@ -110,6 +113,7 @@ func NewHandlers(
|
||||
RawDataExport: implrawdataexport.NewHandler(modules.RawDataExport),
|
||||
Services: implservices.NewHandler(modules.Services),
|
||||
MetricsExplorer: implmetricsexplorer.NewHandler(modules.MetricsExplorer),
|
||||
MetricReductionRule: implmetricreductionrule.NewHandler(modules.MetricReductionRule),
|
||||
InfraMonitoring: implinframonitoring.NewHandler(modules.InfraMonitoring),
|
||||
SpanPercentile: implspanpercentile.NewHandler(modules.SpanPercentile),
|
||||
Global: signozglobal.NewHandler(global),
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestNewHandlers(t *testing.T) {
|
||||
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), userRoleStore, flagger)
|
||||
|
||||
retentionGetter := implretention.NewGetter(implretention.NewStore(sqlstore))
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, nil, nil, retentionGetter, flagger, tagModule)
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, nil, nil, retentionGetter, flagger, tagModule, nil)
|
||||
|
||||
querierHandler := querier.NewHandler(providerSettings, nil, nil)
|
||||
registryHandler := factory.NewHandler(nil)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule/impllmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/logspipeline"
|
||||
"github.com/SigNoz/signoz/pkg/modules/logspipeline/impllogspipeline"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
@@ -66,33 +67,34 @@ import (
|
||||
)
|
||||
|
||||
type Modules struct {
|
||||
OrgGetter organization.Getter
|
||||
OrgSetter organization.Setter
|
||||
Preference preference.Module
|
||||
UserSetter user.Setter
|
||||
UserGetter user.Getter
|
||||
RetentionGetter retention.Getter
|
||||
SavedView savedview.Module
|
||||
Apdex apdex.Module
|
||||
Dashboard dashboard.Module
|
||||
QuickFilter quickfilter.Module
|
||||
TraceFunnel tracefunnel.Module
|
||||
RawDataExport rawdataexport.Module
|
||||
AuthDomain authdomain.Module
|
||||
Session session.Module
|
||||
Services services.Module
|
||||
SpanPercentile spanpercentile.Module
|
||||
MetricsExplorer metricsexplorer.Module
|
||||
InfraMonitoring inframonitoring.Module
|
||||
Promote promote.Module
|
||||
ServiceAccount serviceaccount.Module
|
||||
CloudIntegration cloudintegration.Module
|
||||
LogsPipeline logspipeline.Module
|
||||
RuleStateHistory rulestatehistory.Module
|
||||
TraceDetail tracedetail.Module
|
||||
SpanMapper spanmapper.Module
|
||||
LLMPricingRule llmpricingrule.Module
|
||||
Tag tag.Module
|
||||
OrgGetter organization.Getter
|
||||
OrgSetter organization.Setter
|
||||
Preference preference.Module
|
||||
UserSetter user.Setter
|
||||
UserGetter user.Getter
|
||||
RetentionGetter retention.Getter
|
||||
SavedView savedview.Module
|
||||
Apdex apdex.Module
|
||||
Dashboard dashboard.Module
|
||||
QuickFilter quickfilter.Module
|
||||
TraceFunnel tracefunnel.Module
|
||||
RawDataExport rawdataexport.Module
|
||||
AuthDomain authdomain.Module
|
||||
Session session.Module
|
||||
Services services.Module
|
||||
SpanPercentile spanpercentile.Module
|
||||
MetricsExplorer metricsexplorer.Module
|
||||
MetricReductionRule metricreductionrule.Module
|
||||
InfraMonitoring inframonitoring.Module
|
||||
Promote promote.Module
|
||||
ServiceAccount serviceaccount.Module
|
||||
CloudIntegration cloudintegration.Module
|
||||
LogsPipeline logspipeline.Module
|
||||
RuleStateHistory rulestatehistory.Module
|
||||
TraceDetail tracedetail.Module
|
||||
SpanMapper spanmapper.Module
|
||||
LLMPricingRule llmpricingrule.Module
|
||||
Tag tag.Module
|
||||
}
|
||||
|
||||
func NewModules(
|
||||
@@ -119,6 +121,7 @@ func NewModules(
|
||||
retentionGetter retention.Getter,
|
||||
fl flagger.Flagger,
|
||||
tagModule tag.Module,
|
||||
metricReductionRule metricreductionrule.Module,
|
||||
) Modules {
|
||||
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
|
||||
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
|
||||
@@ -130,32 +133,33 @@ func NewModules(
|
||||
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
||||
|
||||
return Modules{
|
||||
OrgGetter: orgGetter,
|
||||
OrgSetter: orgSetter,
|
||||
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()),
|
||||
SavedView: implsavedview.NewModule(sqlstore),
|
||||
Apdex: implapdex.NewModule(sqlstore),
|
||||
Dashboard: dashboard,
|
||||
UserSetter: userSetter,
|
||||
UserGetter: userGetter,
|
||||
RetentionGetter: retentionGetter,
|
||||
QuickFilter: quickfilter,
|
||||
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
|
||||
RawDataExport: implrawdataexport.NewModule(querier),
|
||||
AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs),
|
||||
Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter),
|
||||
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
|
||||
Services: implservices.NewModule(querier, telemetryStore),
|
||||
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
|
||||
InfraMonitoring: implinframonitoring.NewModule(telemetryStore, telemetryMetadataStore, querier, providerSettings, config.InfraMonitoring),
|
||||
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
|
||||
ServiceAccount: serviceAccount,
|
||||
LogsPipeline: impllogspipeline.NewModule(sqlstore),
|
||||
RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger)),
|
||||
CloudIntegration: cloudIntegrationModule,
|
||||
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore), providerSettings, config.TraceDetail),
|
||||
SpanMapper: implspanmapper.NewModule(implspanmapper.NewStore(sqlstore), fl),
|
||||
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), fl),
|
||||
Tag: tagModule,
|
||||
OrgGetter: orgGetter,
|
||||
OrgSetter: orgSetter,
|
||||
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()),
|
||||
SavedView: implsavedview.NewModule(sqlstore),
|
||||
Apdex: implapdex.NewModule(sqlstore),
|
||||
Dashboard: dashboard,
|
||||
UserSetter: userSetter,
|
||||
UserGetter: userGetter,
|
||||
RetentionGetter: retentionGetter,
|
||||
QuickFilter: quickfilter,
|
||||
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
|
||||
RawDataExport: implrawdataexport.NewModule(querier),
|
||||
AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs),
|
||||
Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter),
|
||||
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
|
||||
Services: implservices.NewModule(querier, telemetryStore),
|
||||
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
|
||||
MetricReductionRule: metricReductionRule,
|
||||
InfraMonitoring: implinframonitoring.NewModule(telemetryStore, telemetryMetadataStore, querier, providerSettings, config.InfraMonitoring),
|
||||
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
|
||||
ServiceAccount: serviceAccount,
|
||||
LogsPipeline: impllogspipeline.NewModule(sqlstore),
|
||||
RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger)),
|
||||
CloudIntegration: cloudIntegrationModule,
|
||||
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore), providerSettings, config.TraceDetail),
|
||||
SpanMapper: implspanmapper.NewModule(implspanmapper.NewStore(sqlstore)),
|
||||
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore)),
|
||||
Tag: tagModule,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule/implmetricreductionrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention/implretention"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
@@ -63,7 +64,7 @@ func TestNewModules(t *testing.T) {
|
||||
|
||||
retentionGetter := implretention.NewGetter(implretention.NewStore(sqlstore))
|
||||
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, serviceAccount, implcloudintegration.NewModule(), retentionGetter, flagger, tagModule)
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, serviceAccount, implcloudintegration.NewModule(), retentionGetter, flagger, tagModule, implmetricreductionrule.NewModule())
|
||||
|
||||
reflectVal := reflect.ValueOf(modules)
|
||||
for i := 0; i < reflectVal.NumField(); i++ {
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
@@ -68,6 +69,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ dashboard.Module }{},
|
||||
struct{ dashboard.Handler }{},
|
||||
struct{ metricsexplorer.Handler }{},
|
||||
struct{ metricreductionrule.Handler }{},
|
||||
struct{ inframonitoring.Handler }{},
|
||||
struct{ gateway.Handler }{},
|
||||
struct{ fields.Handler }{},
|
||||
|
||||
@@ -215,6 +215,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewRecreateUserDashboardPreferenceFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewMigrateRecurrenceBoundsFactory(sqlstore),
|
||||
sqlmigration.NewAddDashboardViewFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddMetricReductionRulesFactory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -295,6 +296,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
modules.Dashboard,
|
||||
handlers.Dashboard,
|
||||
handlers.MetricsExplorer,
|
||||
handlers.MetricReductionRule,
|
||||
handlers.InfraMonitoring,
|
||||
handlers.GatewayHandler,
|
||||
handlers.Fields,
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/meterreporter"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
@@ -114,6 +115,7 @@ func New(
|
||||
meterReporterProviderFactories func(context.Context, factory.ProviderSettings, flagger.Flagger, licensing.Licensing, telemetrystore.TelemetryStore, retention.Getter, organization.Getter, zeus.Zeus) (factory.NamedMap[factory.ProviderFactory[meterreporter.Reporter, meterreporter.Config]], string),
|
||||
querierHandlerCallback func(factory.ProviderSettings, querier.Querier, analytics.Analytics) querier.Handler,
|
||||
cloudIntegrationCallback func(sqlstore.SQLStore, dashboard.Module, global.Global, zeus.Zeus, gateway.Gateway, licensing.Licensing, serviceaccount.Module, cloudintegration.Config) (cloudintegration.Module, error),
|
||||
metricReductionRuleModuleCallback func(sqlstore.SQLStore, telemetrystore.TelemetryStore, dashboard.Module, queryparser.QueryParser, licensing.Licensing, flagger.Flagger, telemetrytypes.MetadataStore, factory.ProviderSettings, int) metricreductionrule.Module,
|
||||
rulerProviderFactories func(cache.Cache, alertmanager.Alertmanager, sqlstore.SQLStore, telemetrystore.TelemetryStore, telemetrytypes.MetadataStore, prometheus.Prometheus, organization.Getter, rulestatehistory.Module, querier.Querier, queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]],
|
||||
) (*SigNoz, error) {
|
||||
// Initialize instrumentation
|
||||
@@ -464,8 +466,10 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metricReductionRuleModule := metricReductionRuleModuleCallback(sqlstore, telemetrystore, dashboard, queryParser, licensing, flagger, telemetryMetadataStore, providerSettings, config.MetricsExplorer.TelemetryStore.Threads)
|
||||
|
||||
// Initialize all modules
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, userRoleStore, serviceAccount, cloudIntegrationModule, retentionGetter, flagger, tagModule)
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, userRoleStore, serviceAccount, cloudIntegrationModule, retentionGetter, flagger, tagModule, metricReductionRuleModule)
|
||||
|
||||
// Initialize ruler from the variant-specific provider factories
|
||||
rulerInstance, err := factory.NewProviderFromNamedMap(ctx, providerSettings, config.Ruler, rulerProviderFactories(cache, alertmanager, sqlstore, telemetrystore, telemetryMetadataStore, prometheus, orgGetter, modules.RuleStateHistory, querier, queryParser), "signoz")
|
||||
|
||||
90
pkg/sqlmigration/096_add_metric_reduction_rules.go
Normal file
90
pkg/sqlmigration/096_add_metric_reduction_rules.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addMetricReductionRules struct {
|
||||
sqlschema sqlschema.SQLSchema
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewAddMetricReductionRulesFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_metric_reduction_rule"), func(_ context.Context, _ factory.ProviderSettings, _ Config) (SQLMigration, error) {
|
||||
return &addMetricReductionRules{
|
||||
sqlschema: sqlschema,
|
||||
sqlstore: sqlstore,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addMetricReductionRules) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addMetricReductionRules) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
sqls := [][]byte{}
|
||||
|
||||
tableSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
|
||||
Name: "metric_reduction_rule",
|
||||
Columns: []*sqlschema.Column{
|
||||
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "metric_name", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "match_type", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "labels", DataType: sqlschema.DataTypeText, Nullable: false, Default: "'[]'"},
|
||||
{Name: "effective_from", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||
{Name: "created_by", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "updated_by", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
},
|
||||
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
|
||||
ColumnNames: []sqlschema.ColumnName{"id"},
|
||||
},
|
||||
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
|
||||
{
|
||||
ReferencingColumnName: sqlschema.ColumnName("org_id"),
|
||||
ReferencedTableName: sqlschema.TableName("organizations"),
|
||||
ReferencedColumnName: sqlschema.ColumnName("id"),
|
||||
},
|
||||
},
|
||||
})
|
||||
sqls = append(sqls, tableSQLs...)
|
||||
|
||||
indexSQLs := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{
|
||||
TableName: "metric_reduction_rule",
|
||||
ColumnNames: []sqlschema.ColumnName{"org_id", "metric_name"},
|
||||
})
|
||||
sqls = append(sqls, indexSQLs...)
|
||||
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addMetricReductionRules) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -41,6 +41,8 @@ const (
|
||||
SamplesV4ReducedSumTableName = "distributed_samples_v4_reduced_sum_60s"
|
||||
TimeseriesV4ReducedTableName = "distributed_time_series_v4_reduced"
|
||||
TimeseriesV4ReducedLocalTableName = "time_series_v4_reduced"
|
||||
|
||||
ReductionRulesTableName = "distributed_metric_reduction_rules"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
// PostableChecks is the request for GET /api/v2/infra_monitoring/checks.
|
||||
// The single `type` query param selects which infra-monitoring subsection the
|
||||
// readiness check runs for.
|
||||
type PostableChecks struct {
|
||||
Type CheckType `query:"type" required:"true"`
|
||||
}
|
||||
|
||||
// Validate rejects empty/unknown checks types.
|
||||
func (req *PostableChecks) Validate() error {
|
||||
if req == nil {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
|
||||
}
|
||||
|
||||
if req.Type.IsZero() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "type is required")
|
||||
}
|
||||
|
||||
if !slices.Contains(ValidCheckTypes, req.Type) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid type: %s", req.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checks is the response for GET /api/v2/infra_monitoring/checks.
|
||||
//
|
||||
// The three present/missing pairs partition a type's requirements into three
|
||||
// dimensions — default-enabled metrics, optional metrics, required attributes —
|
||||
// each bucketed by the collector component (receiver or processor) that
|
||||
// produces it. Ready is true iff every Missing* array is empty.
|
||||
type Checks struct {
|
||||
Type CheckType `json:"type" required:"true"`
|
||||
Ready bool `json:"ready" required:"true"`
|
||||
PresentDefaultEnabledMetrics []MetricsComponentEntry `json:"presentDefaultEnabledMetrics" required:"true"`
|
||||
PresentOptionalMetrics []MetricsComponentEntry `json:"presentOptionalMetrics" required:"true"`
|
||||
PresentRequiredAttributes []AttributesComponentEntry `json:"presentRequiredAttributes" required:"true"`
|
||||
MissingDefaultEnabledMetrics []MissingMetricsComponentEntry `json:"missingDefaultEnabledMetrics" required:"true"`
|
||||
MissingOptionalMetrics []MissingMetricsComponentEntry `json:"missingOptionalMetrics" required:"true"`
|
||||
MissingRequiredAttributes []MissingAttributesComponentEntry `json:"missingRequiredAttributes" required:"true"`
|
||||
}
|
||||
|
||||
// AssociatedComponent identifies the collector receiver or processor that a
|
||||
// metric or attribute originates from. Name is free-form (e.g. "kubeletstatsreceiver").
|
||||
type AssociatedComponent struct {
|
||||
Type CheckComponentType `json:"type" required:"true"`
|
||||
Name string `json:"name" required:"true"`
|
||||
}
|
||||
|
||||
// MetricsComponentEntry lists metrics that share a single associated component.
|
||||
type MetricsComponentEntry struct {
|
||||
Metrics []string `json:"metrics" required:"true"`
|
||||
AssociatedComponent AssociatedComponent `json:"associatedComponent" required:"true"`
|
||||
}
|
||||
|
||||
// AttributesComponentEntry lists resource attributes that share a single associated component.
|
||||
type AttributesComponentEntry struct {
|
||||
Attributes []string `json:"attributes" required:"true"`
|
||||
AssociatedComponent AssociatedComponent `json:"associatedComponent" required:"true"`
|
||||
}
|
||||
|
||||
// MissingMetricsComponentEntry extends MetricsComponentEntry with a user-facing
|
||||
// message and a docs link for fixing the missing metrics.
|
||||
type MissingMetricsComponentEntry struct {
|
||||
MetricsComponentEntry
|
||||
Message string `json:"message" required:"true"`
|
||||
DocumentationLink string `json:"documentationLink" required:"true"`
|
||||
}
|
||||
|
||||
// MissingAttributesComponentEntry extends AttributesComponentEntry with a user-facing
|
||||
// message and a docs link for fixing the missing attributes.
|
||||
type MissingAttributesComponentEntry struct {
|
||||
AttributesComponentEntry
|
||||
Message string `json:"message" required:"true"`
|
||||
DocumentationLink string `json:"documentationLink" required:"true"`
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
// CheckType identifies a single infra-monitoring subsection (UI tab).
|
||||
// One value per v1/v2 list API we surface in the infra-monitoring section.
|
||||
type CheckType struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
CheckTypeHosts = CheckType{valuer.NewString("hosts")}
|
||||
CheckTypeProcesses = CheckType{valuer.NewString("processes")}
|
||||
CheckTypePods = CheckType{valuer.NewString("pods")}
|
||||
CheckTypeNodes = CheckType{valuer.NewString("nodes")}
|
||||
CheckTypeDeployments = CheckType{valuer.NewString("deployments")}
|
||||
CheckTypeDaemonsets = CheckType{valuer.NewString("daemonsets")}
|
||||
CheckTypeStatefulsets = CheckType{valuer.NewString("statefulsets")}
|
||||
CheckTypeJobs = CheckType{valuer.NewString("jobs")}
|
||||
CheckTypeNamespaces = CheckType{valuer.NewString("namespaces")}
|
||||
CheckTypeClusters = CheckType{valuer.NewString("clusters")}
|
||||
CheckTypeVolumes = CheckType{valuer.NewString("volumes")}
|
||||
)
|
||||
|
||||
func (CheckType) Enum() []any {
|
||||
return []any{
|
||||
CheckTypeHosts,
|
||||
CheckTypeProcesses,
|
||||
CheckTypePods,
|
||||
CheckTypeNodes,
|
||||
CheckTypeDeployments,
|
||||
CheckTypeDaemonsets,
|
||||
CheckTypeStatefulsets,
|
||||
CheckTypeJobs,
|
||||
CheckTypeNamespaces,
|
||||
CheckTypeClusters,
|
||||
CheckTypeVolumes,
|
||||
}
|
||||
}
|
||||
|
||||
var ValidCheckTypes = []CheckType{
|
||||
CheckTypeHosts,
|
||||
CheckTypeProcesses,
|
||||
CheckTypePods,
|
||||
CheckTypeNodes,
|
||||
CheckTypeDeployments,
|
||||
CheckTypeDaemonsets,
|
||||
CheckTypeStatefulsets,
|
||||
CheckTypeJobs,
|
||||
CheckTypeNamespaces,
|
||||
CheckTypeClusters,
|
||||
CheckTypeVolumes,
|
||||
}
|
||||
|
||||
// CheckComponentType tags each AssociatedComponent as either a receiver or a processor.
|
||||
// Only these two values are ever written by the module.
|
||||
type CheckComponentType struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
CheckComponentTypeReceiver = CheckComponentType{valuer.NewString("receiver")}
|
||||
CheckComponentTypeProcessor = CheckComponentType{valuer.NewString("processor")}
|
||||
)
|
||||
|
||||
func (CheckComponentType) Enum() []any {
|
||||
return []any{
|
||||
CheckComponentTypeReceiver,
|
||||
CheckComponentTypeProcessor,
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPostableChecks_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req *PostableChecks
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
req: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty type",
|
||||
req: &PostableChecks{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unknown type",
|
||||
req: &PostableChecks{Type: CheckType{valuer.NewString("foo")}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "hosts",
|
||||
req: &PostableChecks{Type: CheckTypeHosts},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "processes",
|
||||
req: &PostableChecks{Type: CheckTypeProcesses},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "pods",
|
||||
req: &PostableChecks{Type: CheckTypePods},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nodes",
|
||||
req: &PostableChecks{Type: CheckTypeNodes},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "deployments",
|
||||
req: &PostableChecks{Type: CheckTypeDeployments},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "daemonsets",
|
||||
req: &PostableChecks{Type: CheckTypeDaemonsets},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "statefulsets",
|
||||
req: &PostableChecks{Type: CheckTypeStatefulsets},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "jobs",
|
||||
req: &PostableChecks{Type: CheckTypeJobs},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "namespaces",
|
||||
req: &PostableChecks{Type: CheckTypeNamespaces},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "clusters",
|
||||
req: &PostableChecks{Type: CheckTypeClusters},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "volumes",
|
||||
req: &PostableChecks{Type: CheckTypeVolumes},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.req.Validate()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Ast(err, errors.TypeInvalidInput), "expected error to be of type InvalidInput")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidCheckTypes_MatchesEnum ensures the ValidCheckTypes slice
|
||||
// stays in sync with the Enum() list — both must cover every CheckType value.
|
||||
func TestValidCheckTypes_MatchesEnum(t *testing.T) {
|
||||
enum := CheckType{}.Enum()
|
||||
require.Equal(t, len(enum), len(ValidCheckTypes))
|
||||
for i, v := range enum {
|
||||
require.Equal(t, v, ValidCheckTypes[i])
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -53,8 +53,7 @@ var (
|
||||
LLMPricingRuleCacheModeSubtract = LLMPricingRuleCacheMode{valuer.NewString("subtract")}
|
||||
// LLMPricingRuleCacheModeAdditive: cached tokens are reported separately (Anthropic-style).
|
||||
LLMPricingRuleCacheModeAdditive = LLMPricingRuleCacheMode{valuer.NewString("additive")}
|
||||
// LLMPricingRuleCacheModeUnknown: provider behaviour is unknown. buildProcessorConfig
|
||||
// normalizes this to an empty mode in the collector config.
|
||||
// LLMPricingRuleCacheModeUnknown: provider behaviour is unknown; falls back to subtract.
|
||||
LLMPricingRuleCacheModeUnknown = LLMPricingRuleCacheMode{valuer.NewString("unknown")}
|
||||
)
|
||||
|
||||
|
||||
@@ -26,23 +26,24 @@ type LLMPricingRuleProcessorAttrs struct {
|
||||
CacheWrite string `yaml:"cache_write" json:"cache_write"`
|
||||
}
|
||||
|
||||
// LLMPricingRuleProcessorDefaultPricing holds the list of model-specific rules.
|
||||
// LLMPricingRuleProcessorDefaultPricing holds the pricing unit and the list of model-specific rules.
|
||||
type LLMPricingRuleProcessorDefaultPricing struct {
|
||||
Unit string `yaml:"unit" json:"unit"`
|
||||
Rules []LLMPricingRuleProcessor `yaml:"rules" json:"rules"`
|
||||
}
|
||||
|
||||
// LLMPricingRuleProcessor is a single pricing rule inside the processor config.
|
||||
type LLMPricingRuleProcessor struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Pattern []string `yaml:"pattern" json:"pattern"`
|
||||
Cache *LLMPricingRuleProcessorCache `yaml:"cache,omitempty" json:"cache,omitempty"`
|
||||
In float64 `yaml:"in" json:"in"`
|
||||
Out float64 `yaml:"out" json:"out"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Pattern []string `yaml:"pattern" json:"pattern"`
|
||||
Cache LLMPricingRuleProcessorCache `yaml:"cache" json:"cache"`
|
||||
In float64 `yaml:"in" json:"in"`
|
||||
Out float64 `yaml:"out" json:"out"`
|
||||
}
|
||||
|
||||
// LLMPricingRuleProcessorCache describes how cached tokens are accounted for.
|
||||
type LLMPricingRuleProcessorCache struct {
|
||||
Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`
|
||||
Mode string `yaml:"mode" json:"mode"`
|
||||
Read float64 `yaml:"read" json:"read"`
|
||||
Write float64 `yaml:"write" json:"write"`
|
||||
}
|
||||
@@ -60,14 +61,10 @@ type LLMPricingRuleProcessorOutputAttrs struct {
|
||||
func buildProcessorConfig(rules []*LLMPricingRule) *LLMPricingRuleProcessorConfig {
|
||||
pricingRules := make([]LLMPricingRuleProcessor, 0, len(rules))
|
||||
for _, r := range rules {
|
||||
var cache *LLMPricingRuleProcessorCache
|
||||
var cache LLMPricingRuleProcessorCache
|
||||
if r.Pricing.Cache != nil {
|
||||
mode := r.Pricing.Cache.Mode.StringValue()
|
||||
if mode != LLMPricingRuleCacheModeSubtract.StringValue() && mode != LLMPricingRuleCacheModeAdditive.StringValue() {
|
||||
mode = ""
|
||||
}
|
||||
cache = &LLMPricingRuleProcessorCache{
|
||||
Mode: mode,
|
||||
cache = LLMPricingRuleProcessorCache{
|
||||
Mode: r.Pricing.Cache.Mode.StringValue(),
|
||||
Read: r.Pricing.Cache.Read,
|
||||
Write: r.Pricing.Cache.Write,
|
||||
}
|
||||
@@ -90,6 +87,7 @@ func buildProcessorConfig(rules []*LLMPricingRule) *LLMPricingRuleProcessorConfi
|
||||
CacheWrite: GenAIUsageCacheCreationInputTokens,
|
||||
},
|
||||
DefaultPricing: LLMPricingRuleProcessorDefaultPricing{
|
||||
Unit: UnitPerMillionTokens.StringValue(),
|
||||
Rules: pricingRules,
|
||||
},
|
||||
OutputAttrs: LLMPricingRuleProcessorOutputAttrs{
|
||||
|
||||
@@ -41,16 +41,6 @@ func makePricingRule(model string, patterns []string, cacheMode LLMPricingRuleCa
|
||||
}
|
||||
}
|
||||
|
||||
func makePricingRuleNoCache(model string, patterns []string, costIn, costOut float64) *LLMPricingRule {
|
||||
return &LLMPricingRule{
|
||||
Model: model,
|
||||
ModelPattern: StringSlice(patterns),
|
||||
Unit: UnitPerMillionTokens,
|
||||
Pricing: LLMRulePricing{Input: costIn, Output: costOut},
|
||||
Enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateCollectorConfigWithLLMPricingProcessor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -72,24 +62,6 @@ func TestGenerateCollectorConfigWithLLMPricingProcessor(t *testing.T) {
|
||||
rules: nil,
|
||||
expectedFile: "collector_no_rules.yaml",
|
||||
},
|
||||
// A rule without cache pricing omits the cache block entirely so the
|
||||
// collector does not apply any cache cost.
|
||||
{
|
||||
name: "rule_without_cache",
|
||||
rules: []*LLMPricingRule{
|
||||
makePricingRuleNoCache("gpt-4o", []string{"gpt-4o*"}, 5.0, 15.0),
|
||||
},
|
||||
expectedFile: "collector_rule_without_cache.yaml",
|
||||
},
|
||||
// An unknown cache mode still emits the cache block (with prices) but omits
|
||||
// the mode field; the collector handles a missing mode in its default branch.
|
||||
{
|
||||
name: "rule_cache_mode_unknown",
|
||||
rules: []*LLMPricingRule{
|
||||
makePricingRule("gpt-4o", []string{"gpt-4o*"}, LLMPricingRuleCacheModeUnknown, 5.0, 15.0, 2.5, 0),
|
||||
},
|
||||
expectedFile: "collector_rule_cache_mode_unknown.yaml",
|
||||
},
|
||||
}
|
||||
|
||||
input, err := os.ReadFile(filepath.Join("testdata", "collector_baseline.yaml"))
|
||||
|
||||
@@ -11,6 +11,7 @@ processors:
|
||||
cache_read: gen_ai.usage.cache_read.input_tokens
|
||||
cache_write: gen_ai.usage.cache_creation.input_tokens
|
||||
default_pricing:
|
||||
unit: per_million_tokens
|
||||
rules: []
|
||||
output_attrs:
|
||||
in: _signoz.gen_ai.cost_input
|
||||
|
||||
@@ -11,6 +11,7 @@ processors:
|
||||
cache_read: gen_ai.usage.cache_read.input_tokens
|
||||
cache_write: gen_ai.usage.cache_creation.input_tokens
|
||||
default_pricing:
|
||||
unit: per_million_tokens
|
||||
rules: []
|
||||
output_attrs:
|
||||
in: _signoz.gen_ai.cost_input
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
exporters:
|
||||
otlp:
|
||||
endpoint: localhost:4317
|
||||
processors:
|
||||
batch: {}
|
||||
signozllmpricing:
|
||||
attrs:
|
||||
model: gen_ai.request.model
|
||||
in: gen_ai.usage.input_tokens
|
||||
out: gen_ai.usage.output_tokens
|
||||
cache_read: gen_ai.usage.cache_read.input_tokens
|
||||
cache_write: gen_ai.usage.cache_creation.input_tokens
|
||||
default_pricing:
|
||||
rules:
|
||||
- name: gpt-4o
|
||||
pattern:
|
||||
- gpt-4o*
|
||||
cache:
|
||||
read: 2.5
|
||||
write: 0
|
||||
in: 5
|
||||
out: 15
|
||||
output_attrs:
|
||||
in: _signoz.gen_ai.cost_input
|
||||
out: _signoz.gen_ai.cost_output
|
||||
cache_read: _signoz.gen_ai.cost_cache_read
|
||||
cache_write: _signoz.gen_ai.cost_cache_write
|
||||
total: _signoz.gen_ai.total_cost
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc: null
|
||||
service:
|
||||
pipelines:
|
||||
traces:
|
||||
exporters:
|
||||
- otlp
|
||||
processors:
|
||||
- batch
|
||||
- signozllmpricing
|
||||
receivers:
|
||||
- otlp
|
||||
@@ -1,39 +0,0 @@
|
||||
exporters:
|
||||
otlp:
|
||||
endpoint: localhost:4317
|
||||
processors:
|
||||
batch: {}
|
||||
signozllmpricing:
|
||||
attrs:
|
||||
model: gen_ai.request.model
|
||||
in: gen_ai.usage.input_tokens
|
||||
out: gen_ai.usage.output_tokens
|
||||
cache_read: gen_ai.usage.cache_read.input_tokens
|
||||
cache_write: gen_ai.usage.cache_creation.input_tokens
|
||||
default_pricing:
|
||||
rules:
|
||||
- name: gpt-4o
|
||||
pattern:
|
||||
- gpt-4o*
|
||||
in: 5
|
||||
out: 15
|
||||
output_attrs:
|
||||
in: _signoz.gen_ai.cost_input
|
||||
out: _signoz.gen_ai.cost_output
|
||||
cache_read: _signoz.gen_ai.cost_cache_read
|
||||
cache_write: _signoz.gen_ai.cost_cache_write
|
||||
total: _signoz.gen_ai.total_cost
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc: null
|
||||
service:
|
||||
pipelines:
|
||||
traces:
|
||||
exporters:
|
||||
- otlp
|
||||
processors:
|
||||
- batch
|
||||
- signozllmpricing
|
||||
receivers:
|
||||
- otlp
|
||||
@@ -11,6 +11,7 @@ processors:
|
||||
cache_read: gen_ai.usage.cache_read.input_tokens
|
||||
cache_write: gen_ai.usage.cache_creation.input_tokens
|
||||
default_pricing:
|
||||
unit: per_million_tokens
|
||||
rules:
|
||||
- name: gpt-4o
|
||||
pattern:
|
||||
|
||||
59
pkg/types/metricreductionruletypes/list.go
Normal file
59
pkg/types/metricreductionruletypes/list.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package metricreductionruletypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
OrderAsc = Order{valuer.NewString("asc")}
|
||||
OrderDesc = Order{valuer.NewString("desc")}
|
||||
)
|
||||
|
||||
func (Order) Enum() []any {
|
||||
return []any{OrderAsc, OrderDesc}
|
||||
}
|
||||
|
||||
type ReductionRuleOrderBy struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
OrderByMetricName = ReductionRuleOrderBy{valuer.NewString("metric")}
|
||||
OrderByIngestedVolume = ReductionRuleOrderBy{valuer.NewString("ingested_volume")}
|
||||
OrderByReducedVolume = ReductionRuleOrderBy{valuer.NewString("reduced_volume")}
|
||||
OrderByReduction = ReductionRuleOrderBy{valuer.NewString("reduction")}
|
||||
OrderByLastUpdated = ReductionRuleOrderBy{valuer.NewString("last_updated")}
|
||||
)
|
||||
|
||||
func (ReductionRuleOrderBy) Enum() []any {
|
||||
return []any{OrderByMetricName, OrderByIngestedVolume, OrderByReducedVolume, OrderByReduction, OrderByLastUpdated}
|
||||
}
|
||||
|
||||
type ListReductionRulesParams struct {
|
||||
OrderBy ReductionRuleOrderBy `query:"orderBy,default=reduction" json:"orderBy"`
|
||||
Order Order `query:"order,default=desc" json:"order"`
|
||||
Search string `query:"search" json:"search"`
|
||||
MetricName string `query:"metricName" json:"metricName,omitempty"`
|
||||
Offset int `query:"offset" json:"offset"`
|
||||
Limit int `query:"limit,default=10" json:"limit"`
|
||||
}
|
||||
|
||||
const maxReductionRulesPageSize = 1000
|
||||
|
||||
func (p *ListReductionRulesParams) Validate() error {
|
||||
if p.Limit <= 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "limit must be greater than 0")
|
||||
}
|
||||
if p.Limit > maxReductionRulesPageSize {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "limit must not exceed %d", maxReductionRulesPageSize)
|
||||
}
|
||||
if p.Offset < 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset must not be negative")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
24
pkg/types/metricreductionruletypes/list_test.go
Normal file
24
pkg/types/metricreductionruletypes/list_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package metricreductionruletypes_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestListReductionRulesParamsSortDefaults(t *testing.T) {
|
||||
var params metricreductionruletypes.ListReductionRulesParams
|
||||
require.NoError(t, binding.Query.BindQuery(map[string][]string{"limit": {"10"}}, ¶ms))
|
||||
|
||||
assert.Equal(t, metricreductionruletypes.OrderByReduction, params.OrderBy, "orderBy defaults to reduction")
|
||||
assert.Equal(t, metricreductionruletypes.OrderDesc, params.Order, "order defaults to desc")
|
||||
}
|
||||
|
||||
func TestListReductionRulesParamsValidate(t *testing.T) {
|
||||
require.Error(t, (&metricreductionruletypes.ListReductionRulesParams{Limit: 0}).Validate(), "limit must be set")
|
||||
require.Error(t, (&metricreductionruletypes.ListReductionRulesParams{Limit: 10, Offset: -1}).Validate(), "offset must not be negative")
|
||||
require.NoError(t, (&metricreductionruletypes.ListReductionRulesParams{Limit: 10}).Validate())
|
||||
}
|
||||
68
pkg/types/metricreductionruletypes/preview.go
Normal file
68
pkg/types/metricreductionruletypes/preview.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package metricreductionruletypes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type AssetType struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
AssetTypeDashboard = AssetType{valuer.NewString("dashboard")}
|
||||
AssetTypeAlert = AssetType{valuer.NewString("alert_rule")}
|
||||
)
|
||||
|
||||
func (AssetType) Enum() []any {
|
||||
return []any{AssetTypeDashboard, AssetTypeAlert}
|
||||
}
|
||||
|
||||
type PostableReductionRulePreview struct {
|
||||
MetricName string `json:"metricName" required:"true"`
|
||||
MatchType MatchType `json:"matchType" required:"true"`
|
||||
Labels []string `json:"labels" required:"true" nullable:"true"`
|
||||
LookbackMs int64 `json:"lookbackMs,omitempty"`
|
||||
}
|
||||
|
||||
func (req *PostableReductionRulePreview) Validate() error {
|
||||
if req == nil {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
|
||||
}
|
||||
if req.MetricName == "" {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName is required")
|
||||
}
|
||||
if req.MatchType != MatchTypeDrop && req.MatchType != MatchTypeKeep {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"matchType must be one of %q or %q", MatchTypeDrop.StringValue(), MatchTypeKeep.StringValue())
|
||||
}
|
||||
if len(req.Labels) == 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "labels must not be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AffectedWidget struct {
|
||||
ID string `json:"id" required:"true"`
|
||||
Name string `json:"name" required:"true"`
|
||||
}
|
||||
|
||||
type AffectedAsset struct {
|
||||
Type AssetType `json:"type" required:"true"`
|
||||
ID string `json:"id" required:"true"`
|
||||
Name string `json:"name" required:"true"`
|
||||
Widget *AffectedWidget `json:"widget,omitempty"`
|
||||
ImpactedLabels []string `json:"impactedLabels" required:"true" nullable:"true"`
|
||||
}
|
||||
|
||||
type GettableReductionRulePreview struct {
|
||||
IngestedSeries uint64 `json:"ingestedSeries" required:"true"`
|
||||
CurrentRetainedSeries uint64 `json:"currentRetainedSeries" required:"true"`
|
||||
RetainedSeries uint64 `json:"retainedSeries" required:"true"`
|
||||
ReductionPercent float64 `json:"reductionPercent" required:"true"`
|
||||
DroppedLabels []string `json:"droppedLabels" required:"true" nullable:"true"`
|
||||
AffectedAssets []AffectedAsset `json:"affectedAssets" required:"true" nullable:"true"`
|
||||
EffectiveFrom time.Time `json:"effectiveFrom" required:"true"`
|
||||
}
|
||||
168
pkg/types/metricreductionruletypes/reduction_rule.go
Normal file
168
pkg/types/metricreductionruletypes/reduction_rule.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package metricreductionruletypes
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeMetricReductionRuleUnsupported = errors.MustNewCode("metric_reduction_rule_unsupported")
|
||||
ErrCodeMetricReductionRuleNotFound = errors.MustNewCode("metric_reduction_rule_not_found")
|
||||
ErrCodeMetricReductionRuleAlreadyExists = errors.MustNewCode("metric_reduction_rule_already_exists")
|
||||
ErrCodeMetricReductionRuleProtectedLabel = errors.MustNewCode("metric_reduction_rule_protected_label")
|
||||
ErrCodeMetricReductionRuleUnsupportedMetricType = errors.MustNewCode("metric_reduction_rule_unsupported_metric_type")
|
||||
)
|
||||
|
||||
type MatchType struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
MatchTypeDrop = MatchType{valuer.NewString("drop")}
|
||||
MatchTypeKeep = MatchType{valuer.NewString("keep")}
|
||||
)
|
||||
|
||||
func (MatchType) Enum() []any {
|
||||
return []any{MatchTypeDrop, MatchTypeKeep}
|
||||
}
|
||||
|
||||
// LabelList is a []string persisted as a single JSON text column.
|
||||
type LabelList []string
|
||||
|
||||
func (l LabelList) Value() (driver.Value, error) {
|
||||
if l == nil {
|
||||
return "[]", nil
|
||||
}
|
||||
b, err := json.Marshal(l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (l *LabelList) Scan(src any) error {
|
||||
var raw []byte
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
raw = []byte(v)
|
||||
case []byte:
|
||||
raw = v
|
||||
case nil:
|
||||
*l = nil
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInternalf(errors.CodeInternal, "metricreductionruletypes: cannot scan %T into LabelList", src)
|
||||
}
|
||||
return json.Unmarshal(raw, l)
|
||||
}
|
||||
|
||||
type ReductionRule struct {
|
||||
bun.BaseModel `bun:"table:metric_reduction_rule" json:"-"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
|
||||
OrgID valuer.UUID `bun:"org_id,type:text,notnull"`
|
||||
MetricName string `bun:"metric_name,type:text,notnull"`
|
||||
MatchType MatchType `bun:"match_type,type:text,notnull"`
|
||||
Labels LabelList `bun:"labels,type:text,notnull,default:'[]'"`
|
||||
EffectiveFrom time.Time `bun:"effective_from,notnull"`
|
||||
}
|
||||
|
||||
func NewReductionRule(orgID valuer.UUID, metricName string, matchType MatchType, labels []string, effectiveFrom time.Time, by string) *ReductionRule {
|
||||
now := time.Now()
|
||||
return &ReductionRule{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
TimeAuditable: types.TimeAuditable{CreatedAt: now, UpdatedAt: now},
|
||||
UserAuditable: types.UserAuditable{CreatedBy: by, UpdatedBy: by},
|
||||
OrgID: orgID,
|
||||
MetricName: metricName,
|
||||
MatchType: matchType,
|
||||
Labels: LabelList(labels),
|
||||
EffectiveFrom: effectiveFrom,
|
||||
}
|
||||
}
|
||||
|
||||
type GettableReductionRule struct {
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
|
||||
MetricName string `json:"metricName" required:"true"`
|
||||
MatchType MatchType `json:"matchType" required:"true"`
|
||||
Labels []string `json:"labels" required:"true" nullable:"true"`
|
||||
EffectiveFrom time.Time `json:"effectiveFrom" required:"true"`
|
||||
Active bool `json:"active" required:"true"`
|
||||
IngestedSeries uint64 `json:"ingestedSeries" required:"true"`
|
||||
RetainedSeries uint64 `json:"retainedSeries" required:"true"`
|
||||
ReductionPercent float64 `json:"reductionPercent" required:"true"`
|
||||
}
|
||||
|
||||
type GettableReductionRules struct {
|
||||
Rules []GettableReductionRule `json:"rules" required:"true" nullable:"true"`
|
||||
Total int `json:"total" required:"true"`
|
||||
}
|
||||
|
||||
type UpdatableReductionRule struct {
|
||||
MatchType MatchType `json:"matchType" required:"true"`
|
||||
Labels []string `json:"labels" required:"true" nullable:"true"`
|
||||
}
|
||||
|
||||
type PostableReductionRule struct {
|
||||
MetricName string `json:"metricName" required:"true"`
|
||||
UpdatableReductionRule
|
||||
}
|
||||
|
||||
var protectedLabels = map[string]struct{}{
|
||||
"le": {},
|
||||
"quantile": {},
|
||||
"__name__": {},
|
||||
"__temporality__": {},
|
||||
"deployment.environment": {},
|
||||
}
|
||||
|
||||
// IsProtectedLabel reports whether a label is always retained regardless of a reduction rule.
|
||||
func IsProtectedLabel(label string) bool {
|
||||
_, ok := protectedLabels[label]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (req *UpdatableReductionRule) Validate() error {
|
||||
if req == nil {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
|
||||
}
|
||||
if req.MatchType != MatchTypeDrop && req.MatchType != MatchTypeKeep {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"matchType must be one of %q or %q", MatchTypeDrop.StringValue(), MatchTypeKeep.StringValue())
|
||||
}
|
||||
if len(req.Labels) == 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"labels must not be empty; to allow all attributes, delete the rule instead")
|
||||
}
|
||||
if req.MatchType == MatchTypeDrop {
|
||||
for _, label := range req.Labels {
|
||||
if IsProtectedLabel(label) {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeMetricReductionRuleProtectedLabel,
|
||||
"label %q is protected and cannot be dropped", label)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (req *PostableReductionRule) Validate() error {
|
||||
if req == nil {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
|
||||
}
|
||||
if req.MetricName == "" {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName is required")
|
||||
}
|
||||
return req.UpdatableReductionRule.Validate()
|
||||
}
|
||||
40
pkg/types/metricreductionruletypes/reduction_rule_test.go
Normal file
40
pkg/types/metricreductionruletypes/reduction_rule_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package metricreductionruletypes_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUpdatableReductionRuleValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
req *metricreductionruletypes.UpdatableReductionRule
|
||||
wantErr bool
|
||||
}{
|
||||
{"nil", nil, true},
|
||||
{"invalid match type", &metricreductionruletypes.UpdatableReductionRule{Labels: []string{"host"}}, true},
|
||||
{"empty labels", &metricreductionruletypes.UpdatableReductionRule{MatchType: metricreductionruletypes.MatchTypeDrop}, true},
|
||||
{"drop protected label", &metricreductionruletypes.UpdatableReductionRule{MatchType: metricreductionruletypes.MatchTypeDrop, Labels: []string{"host", "le"}}, true},
|
||||
{"keep protected label is allowed", &metricreductionruletypes.UpdatableReductionRule{MatchType: metricreductionruletypes.MatchTypeKeep, Labels: []string{"le"}}, false},
|
||||
{"valid drop", &metricreductionruletypes.UpdatableReductionRule{MatchType: metricreductionruletypes.MatchTypeDrop, Labels: []string{"host"}}, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.wantErr {
|
||||
require.Error(t, tc.req.Validate())
|
||||
return
|
||||
}
|
||||
require.NoError(t, tc.req.Validate())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostableReductionRuleValidate(t *testing.T) {
|
||||
valid := metricreductionruletypes.UpdatableReductionRule{MatchType: metricreductionruletypes.MatchTypeKeep, Labels: []string{"host"}}
|
||||
|
||||
require.Error(t, (*metricreductionruletypes.PostableReductionRule)(nil).Validate(), "nil request")
|
||||
require.Error(t, (&metricreductionruletypes.PostableReductionRule{UpdatableReductionRule: valid}).Validate(), "metricName required")
|
||||
require.NoError(t, (&metricreductionruletypes.PostableReductionRule{MetricName: "m", UpdatableReductionRule: valid}).Validate())
|
||||
}
|
||||
8
pkg/types/metricreductionruletypes/stats.go
Normal file
8
pkg/types/metricreductionruletypes/stats.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package metricreductionruletypes
|
||||
|
||||
// GettableReductionRuleStats is the aggregate volume-control summary across all of an org's rules.
|
||||
type GettableReductionRuleStats struct {
|
||||
IngestedSeries uint64 `json:"ingestedSeries" required:"true"`
|
||||
RetainedSeries uint64 `json:"retainedSeries" required:"true"`
|
||||
EstimatedMonthlySavingsUsd float64 `json:"estimatedMonthlySavingsUsd" required:"true"`
|
||||
}
|
||||
17
pkg/types/metricreductionruletypes/store.go
Normal file
17
pkg/types/metricreductionruletypes/store.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package metricreductionruletypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
List(ctx context.Context, orgID valuer.UUID, params *ListReductionRulesParams) ([]*ReductionRule, int, error)
|
||||
Get(ctx context.Context, orgID valuer.UUID, metricName string) (*ReductionRule, error)
|
||||
GetByID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*ReductionRule, error)
|
||||
Create(ctx context.Context, rule *ReductionRule) error
|
||||
Upsert(ctx context.Context, rule *ReductionRule) error
|
||||
DeleteByID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||
RunInTx(ctx context.Context, cb func(ctx context.Context) error) error
|
||||
}
|
||||
3
tests/integration/testdata/inframonitoring/daemonsets_missing_metrics.jsonl
vendored
Normal file
3
tests/integration/testdata/inframonitoring/daemonsets_missing_metrics.jsonl
vendored
Normal 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}
|
||||
3
tests/integration/testdata/inframonitoring/statefulsets_missing_metrics.jsonl
vendored
Normal file
3
tests/integration/testdata/inframonitoring/statefulsets_missing_metrics.jsonl
vendored
Normal 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}
|
||||
@@ -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}
|
||||
3
tests/integration/testdata/inframonitoring/volumes_missing_metrics.jsonl
vendored
Normal file
3
tests/integration/testdata/inframonitoring/volumes_missing_metrics.jsonl
vendored
Normal 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}
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
[
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
[
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
"""Integration tests for the v2 infra-monitoring checks endpoint.
|
||||
|
||||
GET /api/v2/infra_monitoring/checks?type=<t> reports per-tab readiness:
|
||||
for each collector component it lists which required/optional metrics and
|
||||
required attributes are present vs missing. `ready` is true iff every missing
|
||||
list is empty (optional gaps DO block).
|
||||
|
||||
Presence is checked against distributed_metadata with NO time window
|
||||
(pkg/modules/inframonitoring/implinframonitoring/helpers.go:423,:479): a metric
|
||||
is present iff it was ever ingested; an attribute is present iff it appears as a
|
||||
label on any of that type's spec metrics. So seeding here is purely "make these
|
||||
(metric, label) rows exist" — no start/end, no value math. insert_metrics is
|
||||
function-scoped and truncates metadata on teardown, so (serial suite) each test
|
||||
sees only its own seeds.
|
||||
|
||||
SPECS mirrors pkg/modules/inframonitoring/implinframonitoring/checks_constants.go
|
||||
and is the contract lock: if a Go spec changes, the matching assertion fails.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import Metrics
|
||||
|
||||
ENDPOINT = "/api/v2/infra_monitoring/checks"
|
||||
|
||||
# Component names (checks_constants.go:9-15) + their type + docs link.
|
||||
HMR = "hostmetricsreceiver"
|
||||
KSR = "kubeletstatsreceiver"
|
||||
KCR = "k8sclusterreceiver"
|
||||
RDP = "resourcedetectionprocessor"
|
||||
KAP = "k8sattributesprocessor"
|
||||
|
||||
COMPONENT_TYPE = {HMR: "receiver", KSR: "receiver", KCR: "receiver", RDP: "processor", KAP: "processor"}
|
||||
|
||||
_PODS_OPT = [
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
]
|
||||
|
||||
# Mirror of checkSpecs: type -> {default|optional: {component: [metrics]}, attrs: {component: [attrs]}}.
|
||||
SPECS = {
|
||||
"hosts": {
|
||||
"default": {HMR: ["system.cpu.time", "system.memory.usage", "system.cpu.load_average.15m", "system.filesystem.usage"]},
|
||||
"optional": {},
|
||||
"attrs": {RDP: ["host.name"]},
|
||||
},
|
||||
"processes": {
|
||||
"default": {HMR: ["process.cpu.time", "process.memory.usage"]},
|
||||
"optional": {},
|
||||
"attrs": {HMR: ["process.pid"]},
|
||||
},
|
||||
"pods": {
|
||||
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase"]},
|
||||
"optional": {KSR: list(_PODS_OPT)},
|
||||
"attrs": {KAP: ["k8s.pod.uid"]},
|
||||
},
|
||||
"nodes": {
|
||||
"default": {
|
||||
KSR: ["k8s.node.cpu.usage", "k8s.node.memory.working_set"],
|
||||
KCR: ["k8s.node.allocatable_cpu", "k8s.node.allocatable_memory", "k8s.node.condition_ready", "k8s.pod.phase"],
|
||||
},
|
||||
"optional": {},
|
||||
"attrs": {KAP: ["k8s.node.name"]},
|
||||
},
|
||||
"deployments": {
|
||||
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase", "k8s.deployment.desired", "k8s.deployment.available"]},
|
||||
"optional": {KSR: list(_PODS_OPT)},
|
||||
"attrs": {KAP: ["k8s.deployment.name", "k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
"daemonsets": {
|
||||
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase", "k8s.daemonset.desired_scheduled_nodes", "k8s.daemonset.current_scheduled_nodes"]},
|
||||
"optional": {KSR: list(_PODS_OPT)},
|
||||
"attrs": {KAP: ["k8s.daemonset.name", "k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
"statefulsets": {
|
||||
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase", "k8s.statefulset.desired_pods", "k8s.statefulset.current_pods"]},
|
||||
"optional": {KSR: list(_PODS_OPT)},
|
||||
"attrs": {KAP: ["k8s.statefulset.name", "k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
"jobs": {
|
||||
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase", "k8s.job.desired_successful_pods", "k8s.job.active_pods", "k8s.job.failed_pods", "k8s.job.successful_pods"]},
|
||||
"optional": {KSR: list(_PODS_OPT)},
|
||||
"attrs": {KAP: ["k8s.job.name", "k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
"namespaces": {
|
||||
"default": {KSR: ["k8s.pod.cpu.usage", "k8s.pod.memory.working_set"], KCR: ["k8s.pod.phase"]},
|
||||
"optional": {},
|
||||
"attrs": {KAP: ["k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
"clusters": {
|
||||
"default": {KSR: ["k8s.node.cpu.usage", "k8s.node.memory.working_set"], KCR: ["k8s.node.allocatable_cpu", "k8s.node.allocatable_memory", "k8s.node.condition_ready", "k8s.pod.phase"]},
|
||||
"optional": {},
|
||||
"attrs": {RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
"volumes": {
|
||||
"default": {KSR: ["k8s.volume.available", "k8s.volume.capacity", "k8s.volume.inodes", "k8s.volume.inodes.free", "k8s.volume.inodes.used"]},
|
||||
"optional": {},
|
||||
"attrs": {KAP: ["k8s.persistentvolumeclaim.name", "k8s.namespace.name"], RDP: ["k8s.cluster.name"]},
|
||||
},
|
||||
}
|
||||
|
||||
ALL_TYPES = list(SPECS.keys())
|
||||
|
||||
|
||||
# --- helpers ---
|
||||
|
||||
|
||||
def _all(d: dict) -> list:
|
||||
"""Flatten a {component: [items]} map to a flat list."""
|
||||
return [x for items in d.values() for x in items]
|
||||
|
||||
|
||||
def _all_metrics(t: str) -> list:
|
||||
return _all(SPECS[t]["default"]) + _all(SPECS[t]["optional"])
|
||||
|
||||
|
||||
def _attr_labels(t: str, drop: str | None = None) -> dict:
|
||||
"""Labels carrying every required attr (so they resolve present), minus `drop`."""
|
||||
return {a: f"v-{a}" for a in _all(SPECS[t]["attrs"]) if a != drop}
|
||||
|
||||
|
||||
# Marker label so every seeded metric registers in distributed_metadata even when
|
||||
# `labels` is empty (insert_metrics writes a metadata row per label). Non-spec, so it
|
||||
# is never counted as a present required attribute.
|
||||
_SEED_MARKER = {"test.seed.marker": "1"}
|
||||
|
||||
|
||||
def _seed(insert_metrics, metric_names: list, labels: dict) -> None:
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics([Metrics(metric_name=m, labels={**_SEED_MARKER, **labels}, timestamp=now, value=1.0) for m in metric_names])
|
||||
|
||||
|
||||
def _request(signoz: types.SigNoz, token: str, type_: str | None):
|
||||
params = {} if type_ is None else {"type": type_}
|
||||
return requests.get(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
params=params,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
|
||||
def _grouped(entries: list, field: str) -> dict:
|
||||
"""{component_name: set(items)} from a present/missing entry list; also asserts
|
||||
each entry's associatedComponent.type matches the known component type."""
|
||||
out: dict = {}
|
||||
for e in entries:
|
||||
comp = e["associatedComponent"]
|
||||
assert comp["type"] == COMPONENT_TYPE[comp["name"]], f"wrong type for {comp!r}"
|
||||
out.setdefault(comp["name"], set()).update(e[field])
|
||||
return out
|
||||
|
||||
|
||||
def _exp(d: dict) -> dict:
|
||||
return {comp: set(items) for comp, items in d.items()}
|
||||
|
||||
|
||||
def _check_missing_entries(entries: list) -> None:
|
||||
"""Every missing entry carries a non-empty message + a non-empty docs link
|
||||
(exact link not asserted — links are subject to change)."""
|
||||
for e in entries:
|
||||
assert e["message"], f"empty message: {e!r}"
|
||||
assert e["documentationLink"], f"empty doc link: {e!r}"
|
||||
|
||||
|
||||
# Parametrize cases derived from SPECS.
|
||||
_DEFAULT_CASES = [ # one representative dropped default metric per type
|
||||
pytest.param(t, comp, ms[0], id=f"{t}-{ms[0]}") for t in ALL_TYPES for comp, ms in [next(iter(SPECS[t]["default"].items()))]
|
||||
]
|
||||
_OPTIONAL_CASES = [ # types that have optional metrics
|
||||
pytest.param(t, comp, ms[0], id=f"{t}-{ms[0]}") for t in ALL_TYPES for comp, ms in SPECS[t]["optional"].items() if ms
|
||||
]
|
||||
_ATTR_CASES = [pytest.param(t, comp, a, id=f"{t}-{a}") for t in ALL_TYPES for comp, attrs in SPECS[t]["attrs"].items() for a in attrs]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"type_,err_substr",
|
||||
[
|
||||
pytest.param(None, "type is required", id="missing_type"),
|
||||
pytest.param("foo", "invalid type", id="invalid_type"),
|
||||
],
|
||||
)
|
||||
def test_checks_validation_errors(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
type_,
|
||||
err_substr: str,
|
||||
) -> None:
|
||||
"""Missing/unknown `type` query param → 400 invalid_input."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = _request(signoz, token, type_)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
error = response.json()["error"]
|
||||
assert error["code"] == "invalid_input"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("type_", ALL_TYPES)
|
||||
def test_checks_empty_backend(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics, # noqa: ARG001 ensures metadata is truncated around this test
|
||||
type_: str,
|
||||
) -> None:
|
||||
"""No data ingested → not ready; every default metric + required attr reported
|
||||
missing (bucketed by component, with message + docs link); present lists empty."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
data = _request(signoz, token, type_).json()["data"]
|
||||
|
||||
assert data["ready"] is False
|
||||
assert data["presentDefaultEnabledMetrics"] == []
|
||||
assert data["presentOptionalMetrics"] == []
|
||||
assert data["presentRequiredAttributes"] == []
|
||||
assert _grouped(data["missingDefaultEnabledMetrics"], "metrics") == _exp(SPECS[type_]["default"])
|
||||
assert _grouped(data["missingOptionalMetrics"], "metrics") == _exp(SPECS[type_]["optional"])
|
||||
assert _grouped(data["missingRequiredAttributes"], "attributes") == _exp(SPECS[type_]["attrs"])
|
||||
_check_missing_entries(data["missingDefaultEnabledMetrics"])
|
||||
_check_missing_entries(data["missingOptionalMetrics"])
|
||||
_check_missing_entries(data["missingRequiredAttributes"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("type_", ALL_TYPES)
|
||||
def test_checks_all_present_ready(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
type_: str,
|
||||
) -> None:
|
||||
"""Every default+optional metric seeded carrying all required attrs → ready;
|
||||
present buckets exactly match the spec, all missing lists empty."""
|
||||
_seed(insert_metrics, _all_metrics(type_), _attr_labels(type_))
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
data = _request(signoz, token, type_).json()["data"]
|
||||
|
||||
assert data["type"] == type_
|
||||
assert data["ready"] is True
|
||||
assert data["missingDefaultEnabledMetrics"] == []
|
||||
assert data["missingOptionalMetrics"] == []
|
||||
assert data["missingRequiredAttributes"] == []
|
||||
assert _grouped(data["presentDefaultEnabledMetrics"], "metrics") == _exp(SPECS[type_]["default"])
|
||||
assert _grouped(data["presentOptionalMetrics"], "metrics") == _exp(SPECS[type_]["optional"])
|
||||
assert _grouped(data["presentRequiredAttributes"], "attributes") == _exp(SPECS[type_]["attrs"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("type_,component,metric", _DEFAULT_CASES)
|
||||
def test_checks_missing_default_metric(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
type_: str,
|
||||
component: str,
|
||||
metric: str,
|
||||
) -> None:
|
||||
"""One default metric never ingested (everything else present) → that metric is
|
||||
in missingDefaultEnabledMetrics under its component; not ready."""
|
||||
seed = [m for m in _all_metrics(type_) if m != metric]
|
||||
_seed(insert_metrics, seed, _attr_labels(type_))
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
data = _request(signoz, token, type_).json()["data"]
|
||||
|
||||
assert data["ready"] is False
|
||||
assert metric in _grouped(data["missingDefaultEnabledMetrics"], "metrics").get(component, set())
|
||||
assert data["missingOptionalMetrics"] == []
|
||||
assert data["missingRequiredAttributes"] == []
|
||||
_check_missing_entries(data["missingDefaultEnabledMetrics"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("type_,component,metric", _OPTIONAL_CASES)
|
||||
def test_checks_missing_optional_metric(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
type_: str,
|
||||
component: str,
|
||||
metric: str,
|
||||
) -> None:
|
||||
"""One optional metric missing → reported in missingOptionalMetrics and (locked
|
||||
decision) NOT ready, even though all default metrics + attrs are present."""
|
||||
seed = [m for m in _all_metrics(type_) if m != metric]
|
||||
_seed(insert_metrics, seed, _attr_labels(type_))
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
data = _request(signoz, token, type_).json()["data"]
|
||||
|
||||
assert data["ready"] is False
|
||||
assert metric in _grouped(data["missingOptionalMetrics"], "metrics").get(component, set())
|
||||
assert data["missingDefaultEnabledMetrics"] == []
|
||||
assert data["missingRequiredAttributes"] == []
|
||||
_check_missing_entries(data["missingOptionalMetrics"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("type_,component,attr", _ATTR_CASES)
|
||||
def test_checks_missing_required_attribute(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
type_: str,
|
||||
component: str,
|
||||
attr: str,
|
||||
) -> None:
|
||||
"""All metrics present but one required attr never seen on any of them → that
|
||||
attr is in missingRequiredAttributes under its component; not ready."""
|
||||
_seed(insert_metrics, _all_metrics(type_), _attr_labels(type_, drop=attr))
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
data = _request(signoz, token, type_).json()["data"]
|
||||
|
||||
assert data["ready"] is False
|
||||
assert attr in _grouped(data["missingRequiredAttributes"], "attributes").get(component, set())
|
||||
assert data["missingDefaultEnabledMetrics"] == []
|
||||
assert data["missingOptionalMetrics"] == []
|
||||
_check_missing_entries(data["missingRequiredAttributes"])
|
||||
Reference in New Issue
Block a user