mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-25 17:40:32 +01:00
Compare commits
8 Commits
platform-p
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ac07d3d37 | ||
|
|
9bab8e0ae2 | ||
|
|
8040f222ad | ||
|
|
a45212cb79 | ||
|
|
a609a4044c | ||
|
|
f78d98ea71 | ||
|
|
f60e5039be | ||
|
|
a483ef81a4 |
17
.github/workflows/goci.yaml
vendored
17
.github/workflows/goci.yaml
vendored
@@ -140,3 +140,20 @@ jobs:
|
||||
run: |
|
||||
go run cmd/enterprise/*.go generate config web-settings
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in web settings schema. Run go run cmd/enterprise/*.go generate config web-settings locally and commit."; exit 1)
|
||||
transaction-groups:
|
||||
if: |
|
||||
github.event_name == 'merge_group' ||
|
||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: self-checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: go-install
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
- name: generate-transaction-groups
|
||||
run: |
|
||||
go run cmd/enterprise/*.go generate config transaction-groups
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in transaction groups schema. Run go run cmd/enterprise/*.go generate config transaction-groups locally and commit."; exit 1)
|
||||
|
||||
@@ -6,12 +6,15 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/web"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
const webSettingsSchemaPath = "docs/config/web-settings.json"
|
||||
const webSettingsSchemaPath = "frontend/src/schemas/generated/webSettings.schema.json"
|
||||
|
||||
const transactionGroupsSchemaPath = "frontend/src/schemas/generated/transactionGroups.schema.json"
|
||||
|
||||
func registerGenerateConfig(parentCmd *cobra.Command) {
|
||||
configCmd := &cobra.Command{
|
||||
@@ -27,6 +30,14 @@ func registerGenerateConfig(parentCmd *cobra.Command) {
|
||||
},
|
||||
})
|
||||
|
||||
configCmd.AddCommand(&cobra.Command{
|
||||
Use: "transaction-groups",
|
||||
Short: "Generate JSON Schema for transaction groups",
|
||||
RunE: func(currCmd *cobra.Command, args []string) error {
|
||||
return generateTransactionGroups()
|
||||
},
|
||||
})
|
||||
|
||||
parentCmd.AddCommand(configCmd)
|
||||
}
|
||||
|
||||
@@ -52,6 +63,7 @@ func generateWebSettings() error {
|
||||
return err
|
||||
}
|
||||
|
||||
schema.WithTitle("WebSettings")
|
||||
data, err := json.MarshalIndent(schema, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -59,3 +71,31 @@ func generateWebSettings() error {
|
||||
|
||||
return os.WriteFile(webSettingsSchemaPath, append(data, '\n'), 0o600)
|
||||
}
|
||||
|
||||
func generateTransactionGroups() error {
|
||||
falseVal := false
|
||||
noAdditional := jsonschema.SchemaOrBool{TypeBoolean: &falseVal}
|
||||
|
||||
reflector := jsonschema.Reflector{}
|
||||
reflector.DefaultOptions = append(reflector.DefaultOptions,
|
||||
jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (bool, error) {
|
||||
if params.Value.Kind() == reflect.Struct {
|
||||
params.Schema.AdditionalProperties = &noAdditional
|
||||
}
|
||||
return false, nil
|
||||
}),
|
||||
)
|
||||
|
||||
schema, err := reflector.Reflect(authtypes.TransactionGroups{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schema.WithTitle("TransactionGroups")
|
||||
data, err := json.MarshalIndent(schema, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(transactionGroupsSchemaPath, append(data, '\n'), 0o600)
|
||||
}
|
||||
|
||||
@@ -1,48 +1,76 @@
|
||||
# Migrating from the install script to Foundry
|
||||
# Migrating from the install script and `deploy/` to Foundry
|
||||
|
||||
The install script (`install.sh`) and the bundled Compose and Swarm files
|
||||
under `deploy/` are deprecated in favor of [Foundry][foundry], the supported
|
||||
way to install and manage SigNoz. This guide moves an existing Docker Compose
|
||||
or Docker Swarm deployment to Foundry and reattaches your existing volumes, so
|
||||
your data is preserved.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The install script is now deprecated and will no longer receive updates.
|
||||
> This guide is only for **existing** `install.sh` / `deploy/` deployments.
|
||||
> Setting up SigNoz for the first time? Skip migration and install Foundry
|
||||
> directly: [SigNoz install docs][install-docs].
|
||||
|
||||
This guide walks you through migrating an existing SigNoz deployment running via
|
||||
Docker Compose to [Foundry](https://signoz.io/docs/install/docker/).
|
||||
## How it works
|
||||
|
||||
> [!NOTE]
|
||||
> Setting up SigNoz for the first time? You don't need this guide — follow the [SigNoz installation docs](https://signoz.io/docs/install/) instead.
|
||||
Foundry splits a deployment into two commands:
|
||||
|
||||
## Overview
|
||||
To stay up to date on new installation platforms and patterns, please refer to [Foundry](https://github.com/SigNoz/foundry).
|
||||
- `foundryctl forge` generates the deployment manifests from a `casting.yaml`.
|
||||
It never touches running containers, so it is safe to re-run while you
|
||||
iterate.
|
||||
- `foundryctl cast` applies those manifests: it (re)creates the containers and
|
||||
reuses the volumes you point it at.
|
||||
|
||||
Two `foundryctl` commands are used throughout this guide:
|
||||
- **`forge`** — generates deployment manifests from your `casting.yaml`. It does not touch running containers, so it is safe to re-run while you iterate.
|
||||
- **`cast`** — applies the generated manifests: it creates and starts the containers (and pulls new images).
|
||||
You write one `casting.yaml`, point a few patches at your existing data
|
||||
volumes, then cast. The steps below are the same for Compose and Swarm; they
|
||||
differ only in the casting (step 3) and how you stop the old stack (step 5).
|
||||
|
||||
## Prerequisites
|
||||
- [ ] Install Foundry - `curl -fsSL https://signoz.io/foundry.sh | bash`
|
||||
|
||||
## Migration Steps
|
||||
> [!WARNING]
|
||||
> **Before proceeding, back up both:**
|
||||
> - **Your docker volumes** — these hold your data.
|
||||
> - **Your existing `docker-compose.yaml` (and any config it references)** — keep a copy somewhere safe. The compose manifests are no longer distributed by SigNoz, so this backup is your only way to roll back to your previous setup.
|
||||
- An existing SigNoz deployment from `install.sh` or `deploy/` (Compose or
|
||||
Swarm).
|
||||
- `foundryctl` (installed in step 1).
|
||||
|
||||
1. Make a note of the volume names used by your existing deployment for the following components:
|
||||
- ClickHouse
|
||||
- SigNoz
|
||||
- ZooKeeper
|
||||
## Migrate
|
||||
|
||||
> If you used the docker compose file we provided, the volumes will be `signoz-clickhouse`, `signoz-sqlite`, and `signoz-zookeeper-1`.
|
||||
### 1. Install Foundry
|
||||
|
||||
2. Generate your `casting.yaml`. Based on internal testing, the following casting should generate the manifests that mimic the legacy docker compose setup (compare against your backed-up `docker-compose.yaml`). Once created, run `foundryctl forge -f casting.yaml`.
|
||||
```bash
|
||||
curl -fsSL https://signoz.io/foundry.sh | bash
|
||||
```
|
||||
|
||||
Foundry has a [Docker Compose example](https://github.com/SigNoz/foundry/tree/main/docs/examples/docker/compose). Please use it as a reference.
|
||||
### 2. Keep your rollback path
|
||||
|
||||
> [!WARNING]
|
||||
> If your deployment had more than 1 shard or replica, you will need to adjust your manifest volumes accordingly.
|
||||
This migration reattaches your existing volumes in place; it does not move or
|
||||
delete your data. The only destructive action is passing `--volumes` / `-v`
|
||||
when you stop the old stack (step 5), so avoid that flag.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The `replica` and `shard` macros below are placeholders. Replace them with the values from your existing ClickHouse configuration (check the `macros` section of your current ClickHouse config, e.g. `config.xml`/`metrika.xml`), otherwise the generated manifests will not match your existing data.
|
||||
> Keep a copy of your existing `docker-compose.yaml` / stack file (and any
|
||||
> config it references). SigNoz no longer distributes these files, so this copy
|
||||
> is your only way to roll back.
|
||||
|
||||
### 3. Write your `casting.yaml`
|
||||
|
||||
Use the casting for your deployment. Both reproduce the legacy single-node
|
||||
setup (ClickHouse + ZooKeeper + SQLite) and reattach your existing volumes;
|
||||
they differ only in `spec.deployment.flavor` and the volume-reuse patch
|
||||
(Compose volumes have a `name` to replace; Swarm volumes are bare, so the whole
|
||||
entry is replaced). If your deployment ran more than one shard or replica,
|
||||
adjust the volume patches accordingly. The
|
||||
[Docker Compose example][compose-example] is a useful reference.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The `replica` and `shard` macros are placeholders. Replace them with the
|
||||
> values from your existing ClickHouse config (the `macros` section of
|
||||
> `config.xml` / `metrika.xml`), or the generated manifests will not match your
|
||||
> existing data.
|
||||
|
||||
<details>
|
||||
<summary><b>Docker Compose</b> casting.yaml</summary>
|
||||
|
||||
```yaml
|
||||
# casting.yaml
|
||||
apiVersion: v1alpha1
|
||||
kind: Installation
|
||||
metadata:
|
||||
@@ -61,8 +89,8 @@ spec:
|
||||
data:
|
||||
config-0-0.yaml: |
|
||||
macros:
|
||||
replica: "example01-01-1" # replace with your existing ClickHouse replica macro (see legacy configuration files for reference)
|
||||
shard: "01" # replace with your existing ClickHouse shard macro (see legacy configuration files for reference)
|
||||
replica: "example01-01-1" # replace with your replica macro
|
||||
shard: "01" # replace with your shard macro
|
||||
patches:
|
||||
- target: "deployment/compose.yaml"
|
||||
operations:
|
||||
@@ -80,50 +108,163 @@ spec:
|
||||
value: root
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The `user: root` patch on the ZooKeeper service is required so the container can read/write the data in your reused ZooKeeper volume, which was created with `root`-owned files by the legacy compose setup. Without it, ZooKeeper may fail to start with permission errors.
|
||||
</details>
|
||||
|
||||
If you had custom configurations for features like SMTP or additional ingestion processors/receivers, you will need to include those in your casting file via [patches](https://github.com/SigNoz/foundry/blob/main/docs/concepts/patches.md), [custom configuration](https://github.com/SigNoz/foundry/blob/main/docs/concepts/moldings.md#custom-config-files) or [environment variables](https://github.com/SigNoz/foundry/blob/main/docs/reference/casting-file.md#molding-spec) based on your previous configuration.
|
||||
<details>
|
||||
<summary><b>Docker Swarm</b> casting.yaml</summary>
|
||||
|
||||
3. Review your manifests, we suggest executing the following checks on your manifests before proceeding:
|
||||
- [ ] Validate the container images match what your deployment had, Foundry uses `latest` on generation by default.
|
||||
- [ ] If your signoz version was older than latest, please check the [upgrade path](https://signoz.io/docs/operate/upgrade/) first.
|
||||
- [ ] Check the produced manifests in `pours/deployment` match your older configurations. Extra consideration and review needs to be done on `compose.yaml` as this will be the main entry point for your new deployment.
|
||||
- [ ] The configuration files for clickhouse are now in YAML so validate your custom settings are present.
|
||||
```yaml
|
||||
# casting.yaml
|
||||
apiVersion: v1alpha1
|
||||
kind: Installation
|
||||
metadata:
|
||||
name: signoz
|
||||
spec:
|
||||
deployment:
|
||||
flavor: swarm
|
||||
mode: docker
|
||||
metastore:
|
||||
kind: sqlite
|
||||
telemetrykeeper:
|
||||
kind: zookeeper
|
||||
telemetrystore:
|
||||
spec:
|
||||
config:
|
||||
data:
|
||||
config-0-0.yaml: |
|
||||
macros:
|
||||
replica: "example01-01-1" # replace with your replica macro
|
||||
shard: "01" # replace with your shard macro
|
||||
patches:
|
||||
- target: "deployment/compose.yaml"
|
||||
operations:
|
||||
- op: replace
|
||||
path: /volumes/signoz-telemetrykeeper-0-data
|
||||
value:
|
||||
name: signoz-zookeeper-1
|
||||
- op: replace
|
||||
path: /volumes/signoz-telemetrystore-0-0-data
|
||||
value:
|
||||
name: signoz-clickhouse
|
||||
- op: replace
|
||||
path: /volumes/signoz-metastore-sqlite-0-data
|
||||
value:
|
||||
name: signoz-sqlite
|
||||
- op: add
|
||||
path: /services/signoz-telemetrykeeper-zookeeper-0/user
|
||||
value: root
|
||||
```
|
||||
|
||||
4. Execute a `docker compose down`. **Do not** include parameters such as `--volumes` (or `-v`), as it will wipe the volumes we need to maintain and reuse to avoid data loss.
|
||||
</details>
|
||||
|
||||
> [!NOTE]
|
||||
> This will generate downtime so please plan accordingly.
|
||||
> The `user: root` patch on the ZooKeeper service lets the container read and
|
||||
> write the data in your reused ZooKeeper volume, whose files the legacy setup
|
||||
> created as `root`. Without it, ZooKeeper may fail to start with permission
|
||||
> errors.
|
||||
|
||||
5. Validate the SigNoz containers are down with `docker ps -a`. Multiple containers cannot bind the same volume.
|
||||
If you had custom configuration (SMTP, extra ingestion receivers/processors,
|
||||
or custom ClickHouse settings), carry it over via [patches][patches],
|
||||
[custom config files][custom-config], or [environment variables][env-vars].
|
||||
|
||||
6. Run `foundryctl cast -f casting.yaml`. This will recreate the containers based on the spec. This process will download new container images.
|
||||
### 4. Generate and review the manifests
|
||||
|
||||
```bash
|
||||
foundryctl forge -f casting.yaml
|
||||
```
|
||||
|
||||
Review `pours/deployment/` before deploying:
|
||||
|
||||
- [ ] Container images match your current deployment. Foundry generates with
|
||||
`latest` by default; if your SigNoz version was older than latest, check the
|
||||
[upgrade path][upgrade-path] first.
|
||||
- [ ] The generated manifests match your previous configuration, especially
|
||||
`compose.yaml` (the new entry point for your deployment).
|
||||
- [ ] The ClickHouse config is now YAML rather than XML; confirm your custom
|
||||
settings carried over (see [ClickHouse configuration files][ch-config] for
|
||||
the XML-to-YAML mapping).
|
||||
|
||||
### 5. Stop the old deployment
|
||||
|
||||
Use the command for your deployment. Do **not** pass `--volumes` / `-v`; that
|
||||
would delete the data you are migrating.
|
||||
|
||||
```bash
|
||||
docker compose down # Compose
|
||||
docker stack rm signoz # Swarm
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> When `cast` is run, the migration container will execute its migrations.
|
||||
> This causes downtime, so plan accordingly.
|
||||
|
||||
## Verifying the Migration
|
||||
- SigNoz containers will be up and running.
|
||||
- Log in to the SigNoz UI and verify that data is present.
|
||||
- Signoz will run on localhost:8080
|
||||
- Validate that your data ingestion is receiving data.
|
||||
- Ingesters will receive data on localhost:4317(grpc) and localhost:4318(http)
|
||||
- Review the logs from both ClickHouse and ZooKeeper; no errors should be present.
|
||||
Confirm nothing is still bound to the volumes before continuing:
|
||||
|
||||
## Rolling Back
|
||||
Because step 4 brought the legacy stack down *without* `-v`, your original volumes
|
||||
are untouched and still hold your data. To roll back:
|
||||
```bash
|
||||
docker ps -a
|
||||
```
|
||||
|
||||
- Stop and remove the containers created by Foundry (`docker compose down`, again without `-v`).
|
||||
- Confirm the containers are gone with `docker ps -a` so nothing else is bound to the volumes.
|
||||
- Reapply your original docker compose file (`docker compose up -d`). It will reattach to the
|
||||
existing volumes and restore your prior state.
|
||||
### 6. Deploy with Foundry
|
||||
|
||||
```bash
|
||||
foundryctl cast -f casting.yaml
|
||||
```
|
||||
|
||||
This recreates the containers against your existing volumes and pulls the
|
||||
images. The migration container runs the schema migrations as part of `cast`.
|
||||
|
||||
**Prefer not to use `cast`?** The manifests in `pours/deployment/` are standard
|
||||
Docker artifacts you can apply yourself. Run the command from that directory so
|
||||
the relative config paths resolve:
|
||||
|
||||
```bash
|
||||
cd pours/deployment
|
||||
docker compose up -d # Compose
|
||||
docker stack deploy -c compose.yaml signoz # Swarm
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
- All SigNoz containers are running.
|
||||
- The UI is reachable on `http://localhost:8080`, and OTLP on `4317` (gRPC)
|
||||
and `4318` (HTTP), so already-instrumented apps and saved bookmarks keep
|
||||
working.
|
||||
- Your existing data is present in the UI, and new data is being ingested.
|
||||
- ClickHouse and ZooKeeper logs show no errors.
|
||||
|
||||
## Roll back
|
||||
|
||||
Step 5 left your volumes untouched, so your data is intact. To return to the
|
||||
previous setup:
|
||||
|
||||
1. Bring down the Foundry deployment (`docker compose down` or
|
||||
`docker stack rm signoz`, again without `-v`).
|
||||
2. Confirm the containers are gone with `docker ps -a`.
|
||||
3. Re-apply your backed-up stack: `docker compose up -d` (Compose) or
|
||||
`docker stack deploy -c docker-compose.yaml signoz` (Swarm). It reattaches
|
||||
the same volumes and restores your prior state.
|
||||
|
||||
## Troubleshooting
|
||||
- Please reach out to our community on [Slack](https://signoz.io/slack).
|
||||
|
||||
If the migration runs into trouble, reach out on [Slack][slack] or open a
|
||||
[Foundry issue][foundry-issues].
|
||||
|
||||
## References
|
||||
- [SigNoz Docker installation docs](https://signoz.io/docs/install/docker/)
|
||||
- [SigNoz documentation](https://signoz.io/docs)
|
||||
- [Foundry](https://github.com/SigNoz/foundry)
|
||||
|
||||
- [Foundry][foundry]
|
||||
- [Casting file reference][casting-ref]
|
||||
- [Custom config files][custom-config]
|
||||
- [Patches][patches]
|
||||
- [SigNoz documentation][signoz-docs]
|
||||
|
||||
[foundry]: https://github.com/SigNoz/foundry
|
||||
[install-docs]: https://signoz.io/docs/install/
|
||||
[compose-example]: https://github.com/SigNoz/foundry/tree/main/docs/examples/docker/compose
|
||||
[patches]: https://github.com/SigNoz/foundry/blob/main/docs/concepts/patches.md
|
||||
[custom-config]: https://github.com/SigNoz/foundry/blob/main/docs/concepts/moldings.md#custom-config-files
|
||||
[env-vars]: https://github.com/SigNoz/foundry/blob/main/docs/reference/casting-file.md#molding-spec
|
||||
[casting-ref]: https://github.com/SigNoz/foundry/blob/main/docs/reference/casting-file.md
|
||||
[ch-config]: https://clickhouse.com/docs/operations/configuration-files
|
||||
[upgrade-path]: https://signoz.io/docs/operate/upgrade/
|
||||
[slack]: https://signoz.io/slack
|
||||
[foundry-issues]: https://github.com/SigNoz/foundry/issues
|
||||
[signoz-docs]: https://signoz.io/docs
|
||||
|
||||
@@ -651,8 +651,6 @@ components:
|
||||
$ref: '#/components/schemas/AuthtypesTransactionGroups'
|
||||
required:
|
||||
- name
|
||||
- description
|
||||
- transactionGroups
|
||||
type: object
|
||||
AuthtypesPostableRotateToken:
|
||||
properties:
|
||||
@@ -2407,6 +2405,46 @@ components:
|
||||
to_user:
|
||||
type: string
|
||||
type: object
|
||||
CoretypesKind:
|
||||
enum:
|
||||
- anonymous
|
||||
- organization
|
||||
- role
|
||||
- serviceaccount
|
||||
- user
|
||||
- notification-channel
|
||||
- route-policy
|
||||
- apdex-setting
|
||||
- auth-domain
|
||||
- session
|
||||
- cloud-integration
|
||||
- cloud-integration-service
|
||||
- integration
|
||||
- dashboard
|
||||
- public-dashboard
|
||||
- ingestion-key
|
||||
- ingestion-limit
|
||||
- pipeline
|
||||
- user-preference
|
||||
- org-preference
|
||||
- quick-filter
|
||||
- ttl-setting
|
||||
- rule
|
||||
- planned-maintenance
|
||||
- saved-view
|
||||
- trace-funnel
|
||||
- factor-password
|
||||
- factor-api-key
|
||||
- license
|
||||
- subscription
|
||||
- logs
|
||||
- traces
|
||||
- metrics
|
||||
- audit-logs
|
||||
- meter-metrics
|
||||
- logs-field
|
||||
- traces-field
|
||||
type: string
|
||||
CoretypesObject:
|
||||
properties:
|
||||
resource:
|
||||
@@ -2448,7 +2486,7 @@ components:
|
||||
CoretypesResourceRef:
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
$ref: '#/components/schemas/CoretypesKind'
|
||||
type:
|
||||
$ref: '#/components/schemas/CoretypesType'
|
||||
required:
|
||||
@@ -3973,6 +4011,94 @@ components:
|
||||
enabled:
|
||||
type: boolean
|
||||
type: object
|
||||
InframonitoringtypesAssociatedComponent:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/InframonitoringtypesCheckComponentType'
|
||||
required:
|
||||
- type
|
||||
- name
|
||||
type: object
|
||||
InframonitoringtypesAttributesComponentEntry:
|
||||
properties:
|
||||
associatedComponent:
|
||||
$ref: '#/components/schemas/InframonitoringtypesAssociatedComponent'
|
||||
attributes:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- attributes
|
||||
- associatedComponent
|
||||
type: object
|
||||
InframonitoringtypesCheckComponentType:
|
||||
enum:
|
||||
- receiver
|
||||
- processor
|
||||
type: string
|
||||
InframonitoringtypesCheckType:
|
||||
enum:
|
||||
- hosts
|
||||
- processes
|
||||
- pods
|
||||
- nodes
|
||||
- deployments
|
||||
- daemonsets
|
||||
- statefulsets
|
||||
- jobs
|
||||
- namespaces
|
||||
- clusters
|
||||
- volumes
|
||||
type: string
|
||||
InframonitoringtypesChecks:
|
||||
properties:
|
||||
missingDefaultEnabledMetrics:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesMissingMetricsComponentEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
missingOptionalMetrics:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesMissingMetricsComponentEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
missingRequiredAttributes:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesMissingAttributesComponentEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
presentDefaultEnabledMetrics:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesMetricsComponentEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
presentOptionalMetrics:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesMetricsComponentEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
presentRequiredAttributes:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesAttributesComponentEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
ready:
|
||||
type: boolean
|
||||
type:
|
||||
$ref: '#/components/schemas/InframonitoringtypesCheckType'
|
||||
required:
|
||||
- type
|
||||
- ready
|
||||
- presentDefaultEnabledMetrics
|
||||
- presentOptionalMetrics
|
||||
- presentRequiredAttributes
|
||||
- missingDefaultEnabledMetrics
|
||||
- missingOptionalMetrics
|
||||
- missingRequiredAttributes
|
||||
type: object
|
||||
InframonitoringtypesClusterRecord:
|
||||
properties:
|
||||
clusterCPU:
|
||||
@@ -4016,8 +4142,6 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesClusterRecord'
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
total:
|
||||
type: integer
|
||||
type:
|
||||
@@ -4028,7 +4152,6 @@ components:
|
||||
- type
|
||||
- records
|
||||
- total
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesDaemonSetRecord:
|
||||
@@ -4085,8 +4208,6 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesDaemonSetRecord'
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
total:
|
||||
type: integer
|
||||
type:
|
||||
@@ -4097,7 +4218,6 @@ components:
|
||||
- type
|
||||
- records
|
||||
- total
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesDeploymentRecord:
|
||||
@@ -4154,8 +4274,6 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesDeploymentRecord'
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
total:
|
||||
type: integer
|
||||
type:
|
||||
@@ -4166,7 +4284,6 @@ components:
|
||||
- type
|
||||
- records
|
||||
- total
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesHostFilter:
|
||||
@@ -4232,8 +4349,6 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesHostRecord'
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
total:
|
||||
type: integer
|
||||
type:
|
||||
@@ -4244,7 +4359,6 @@ components:
|
||||
- type
|
||||
- records
|
||||
- total
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesJobRecord:
|
||||
@@ -4307,8 +4421,6 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesJobRecord'
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
total:
|
||||
type: integer
|
||||
type:
|
||||
@@ -4319,9 +4431,59 @@ components:
|
||||
- type
|
||||
- records
|
||||
- total
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesMetricsComponentEntry:
|
||||
properties:
|
||||
associatedComponent:
|
||||
$ref: '#/components/schemas/InframonitoringtypesAssociatedComponent'
|
||||
metrics:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- metrics
|
||||
- associatedComponent
|
||||
type: object
|
||||
InframonitoringtypesMissingAttributesComponentEntry:
|
||||
properties:
|
||||
associatedComponent:
|
||||
$ref: '#/components/schemas/InframonitoringtypesAssociatedComponent'
|
||||
attributes:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
documentationLink:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- attributes
|
||||
- associatedComponent
|
||||
- message
|
||||
- documentationLink
|
||||
type: object
|
||||
InframonitoringtypesMissingMetricsComponentEntry:
|
||||
properties:
|
||||
associatedComponent:
|
||||
$ref: '#/components/schemas/InframonitoringtypesAssociatedComponent'
|
||||
documentationLink:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
metrics:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- metrics
|
||||
- associatedComponent
|
||||
- message
|
||||
- documentationLink
|
||||
type: object
|
||||
InframonitoringtypesNamespaceRecord:
|
||||
properties:
|
||||
meta:
|
||||
@@ -4354,8 +4516,6 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNamespaceRecord'
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
total:
|
||||
type: integer
|
||||
type:
|
||||
@@ -4366,7 +4526,6 @@ components:
|
||||
- type
|
||||
- records
|
||||
- total
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesNodeCondition:
|
||||
@@ -4431,8 +4590,6 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeRecord'
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
total:
|
||||
type: integer
|
||||
type:
|
||||
@@ -4443,7 +4600,6 @@ components:
|
||||
- type
|
||||
- records
|
||||
- total
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesPodCountsByPhase:
|
||||
@@ -4529,8 +4685,6 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodRecord'
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
total:
|
||||
type: integer
|
||||
type:
|
||||
@@ -4541,7 +4695,6 @@ components:
|
||||
- type
|
||||
- records
|
||||
- total
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesPostableClusters:
|
||||
@@ -4804,16 +4957,6 @@ components:
|
||||
- end
|
||||
- limit
|
||||
type: object
|
||||
InframonitoringtypesRequiredMetricsCheck:
|
||||
properties:
|
||||
missingMetrics:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- missingMetrics
|
||||
type: object
|
||||
InframonitoringtypesResponseType:
|
||||
enum:
|
||||
- list
|
||||
@@ -4873,8 +5016,6 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesStatefulSetRecord'
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
total:
|
||||
type: integer
|
||||
type:
|
||||
@@ -4885,7 +5026,6 @@ components:
|
||||
- type
|
||||
- records
|
||||
- total
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesVolumeRecord:
|
||||
@@ -4933,8 +5073,6 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesVolumeRecord'
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
total:
|
||||
type: integer
|
||||
type:
|
||||
@@ -4945,7 +5083,6 @@ components:
|
||||
- type
|
||||
- records
|
||||
- total
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
LlmpricingruletypesGettablePricingRules:
|
||||
@@ -14830,6 +14967,72 @@ paths:
|
||||
summary: Health check
|
||||
tags:
|
||||
- health
|
||||
/api/v2/infra_monitoring/checks:
|
||||
get:
|
||||
deprecated: false
|
||||
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.'
|
||||
operationId: GetChecks
|
||||
parameters:
|
||||
- in: query
|
||||
name: type
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/components/schemas/InframonitoringtypesCheckType'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/InframonitoringtypesChecks'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Run Infra Monitoring Setup Checks
|
||||
tags:
|
||||
- inframonitoring
|
||||
/api/v2/infra_monitoring/clusters:
|
||||
post:
|
||||
deprecated: false
|
||||
@@ -14844,10 +15047,10 @@ paths:
|
||||
for custom groupBy keys; in both modes every row aggregates nodes and pods
|
||||
in the group. Supports filtering via a filter expression, custom groupBy,
|
||||
ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination
|
||||
via offset/limit. Also reports 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.'
|
||||
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.'
|
||||
operationId: ListClusters
|
||||
requestBody:
|
||||
content:
|
||||
@@ -14920,11 +15123,11 @@ paths:
|
||||
row aggregates pods owned by daemonsets in the group. Supports filtering via
|
||||
a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit
|
||||
/ memory / memory_request / memory_limit / desired_nodes / current_nodes,
|
||||
and pagination via offset/limit. Also reports 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.'
|
||||
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.'
|
||||
operationId: ListDaemonSets
|
||||
requestBody:
|
||||
content:
|
||||
@@ -14995,11 +15198,11 @@ paths:
|
||||
group. Supports filtering via a filter expression, custom groupBy, ordering
|
||||
by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit
|
||||
/ desired_pods / available_pods, and pagination via offset/limit. Also reports
|
||||
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.'
|
||||
whether the requested time range falls before the data retention boundary.
|
||||
Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit,
|
||||
deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods,
|
||||
availablePods) return -1 as a sentinel when no data is available for that
|
||||
field.'
|
||||
operationId: ListDeployments
|
||||
requestBody:
|
||||
content:
|
||||
@@ -15064,10 +15267,9 @@ paths:
|
||||
custom groupBy to aggregate hosts by any attribute, ordering by any of the
|
||||
five metrics, and pagination via offset/limit. The response type is ''list''
|
||||
for the default host.name grouping or ''grouped_list'' for custom groupBy
|
||||
keys. Also reports 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.'
|
||||
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.'
|
||||
operationId: ListHosts
|
||||
requestBody:
|
||||
content:
|
||||
@@ -15141,11 +15343,11 @@ paths:
|
||||
jobs in the group. Supports filtering via a filter expression, custom groupBy,
|
||||
ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit
|
||||
/ desired_successful_pods / active_pods / failed_pods / successful_pods, and
|
||||
pagination via offset/limit. Also reports 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.'
|
||||
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.'
|
||||
operationId: ListJobs
|
||||
requestBody:
|
||||
content:
|
||||
@@ -15210,10 +15412,10 @@ paths:
|
||||
type is ''list'' for the default k8s.namespace.name grouping or ''grouped_list''
|
||||
for custom groupBy keys; in both modes every row aggregates pods in the group.
|
||||
Supports filtering via a filter expression, custom groupBy, ordering by cpu
|
||||
/ memory, and pagination via offset/limit. Also reports 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.'
|
||||
/ 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.'
|
||||
operationId: ListNamespaces
|
||||
requestBody:
|
||||
content:
|
||||
@@ -15281,10 +15483,10 @@ paths:
|
||||
for custom groupBy keys (each row aggregates nodes in the group; condition
|
||||
stays no_data). Supports filtering via a filter expression, custom groupBy,
|
||||
ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination
|
||||
via offset/limit. Also reports 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.'
|
||||
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.'
|
||||
operationId: ListNodes
|
||||
requestBody:
|
||||
content:
|
||||
@@ -15353,11 +15555,10 @@ paths:
|
||||
is one pod with its current phase) or ''grouped_list'' for custom groupBy
|
||||
keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase:
|
||||
{ pending, running, succeeded, failed, unknown } derived from each pod''s
|
||||
latest phase in the window). Also reports 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.'
|
||||
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.'
|
||||
operationId: ListPods
|
||||
requestBody:
|
||||
content:
|
||||
@@ -15424,11 +15625,10 @@ paths:
|
||||
usage, inodes, inodes_free, inodes_used), and pagination via offset/limit.
|
||||
The response type is ''list'' for the default k8s.persistentvolumeclaim.name
|
||||
grouping or ''grouped_list'' for custom groupBy keys; in both modes every
|
||||
row aggregates volumes in the group. Also reports 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.'
|
||||
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.'
|
||||
operationId: ListVolumes
|
||||
requestBody:
|
||||
content:
|
||||
@@ -15499,11 +15699,10 @@ paths:
|
||||
statefulsets in the group. Supports filtering via a filter expression, custom
|
||||
groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request
|
||||
/ memory_limit / desired_pods / current_pods, and pagination via offset/limit.
|
||||
Also reports 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.'
|
||||
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.'
|
||||
operationId: ListStatefulSets
|
||||
requestBody:
|
||||
content:
|
||||
@@ -15662,16 +15861,20 @@ paths:
|
||||
summary: List metric names
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/{metric_name}/alerts:
|
||||
/api/v2/metrics/alerts:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns associated alerts for a specified metric
|
||||
operationId: GetMetricAlerts
|
||||
parameters:
|
||||
- in: path
|
||||
name: metric_name
|
||||
- description: The name of the metric. May contain slashes (e.g. cloud-provider
|
||||
metrics like run.googleapis.com/request_latencies).
|
||||
in: query
|
||||
name: metricName
|
||||
required: true
|
||||
schema:
|
||||
description: The name of the metric. May contain slashes (e.g. cloud-provider
|
||||
metrics like run.googleapis.com/request_latencies).
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
@@ -15726,28 +15929,36 @@ paths:
|
||||
summary: Get metric alerts
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/{metric_name}/attributes:
|
||||
/api/v2/metrics/attributes:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns attribute keys and their unique values for
|
||||
a specified metric
|
||||
operationId: GetMetricAttributes
|
||||
parameters:
|
||||
- in: query
|
||||
name: start
|
||||
schema:
|
||||
nullable: true
|
||||
type: integer
|
||||
- in: query
|
||||
name: end
|
||||
schema:
|
||||
nullable: true
|
||||
type: integer
|
||||
- in: path
|
||||
name: metric_name
|
||||
- description: The name of the metric. May contain slashes (e.g. cloud-provider
|
||||
metrics like run.googleapis.com/request_latencies).
|
||||
in: query
|
||||
name: metricName
|
||||
required: true
|
||||
schema:
|
||||
description: The name of the metric. May contain slashes (e.g. cloud-provider
|
||||
metrics like run.googleapis.com/request_latencies).
|
||||
type: string
|
||||
- description: Start of the time range as a Unix timestamp in milliseconds.
|
||||
in: query
|
||||
name: start
|
||||
schema:
|
||||
description: Start of the time range as a Unix timestamp in milliseconds.
|
||||
nullable: true
|
||||
type: integer
|
||||
- description: End of the time range as a Unix timestamp in milliseconds.
|
||||
in: query
|
||||
name: end
|
||||
schema:
|
||||
description: End of the time range as a Unix timestamp in milliseconds.
|
||||
nullable: true
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
@@ -15801,16 +16012,20 @@ paths:
|
||||
summary: Get metric attributes
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/{metric_name}/dashboards:
|
||||
/api/v2/metrics/dashboards:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns associated dashboards for a specified metric
|
||||
operationId: GetMetricDashboards
|
||||
parameters:
|
||||
- in: path
|
||||
name: metric_name
|
||||
- description: The name of the metric. May contain slashes (e.g. cloud-provider
|
||||
metrics like run.googleapis.com/request_latencies).
|
||||
in: query
|
||||
name: metricName
|
||||
required: true
|
||||
schema:
|
||||
description: The name of the metric. May contain slashes (e.g. cloud-provider
|
||||
metrics like run.googleapis.com/request_latencies).
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
@@ -15865,17 +16080,21 @@ paths:
|
||||
summary: Get metric dashboards
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/{metric_name}/highlights:
|
||||
/api/v2/metrics/highlights:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns highlights like number of datapoints, totaltimeseries,
|
||||
active time series, last received time for a specified metric
|
||||
operationId: GetMetricHighlights
|
||||
parameters:
|
||||
- in: path
|
||||
name: metric_name
|
||||
- description: The name of the metric. May contain slashes (e.g. cloud-provider
|
||||
metrics like run.googleapis.com/request_latencies).
|
||||
in: query
|
||||
name: metricName
|
||||
required: true
|
||||
schema:
|
||||
description: The name of the metric. May contain slashes (e.g. cloud-provider
|
||||
metrics like run.googleapis.com/request_latencies).
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
@@ -15930,17 +16149,79 @@ paths:
|
||||
summary: Get metric highlights
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/{metric_name}/metadata:
|
||||
/api/v2/metrics/inspect:
|
||||
post:
|
||||
deprecated: false
|
||||
description: Returns raw time series data points for a metric within a time
|
||||
range (max 30 minutes). Each series includes labels and timestamp/value pairs.
|
||||
operationId: InspectMetrics
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesInspectMetricsRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesInspectMetricsResponse'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Inspect raw metric data points
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/metadata:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns metadata information like metric description,
|
||||
unit, type, temporality, monotonicity for a specified metric
|
||||
operationId: GetMetricMetadata
|
||||
parameters:
|
||||
- in: path
|
||||
name: metric_name
|
||||
- description: The name of the metric. May contain slashes (e.g. cloud-provider
|
||||
metrics like run.googleapis.com/request_latencies).
|
||||
in: query
|
||||
name: metricName
|
||||
required: true
|
||||
schema:
|
||||
description: The name of the metric. May contain slashes (e.g. cloud-provider
|
||||
metrics like run.googleapis.com/request_latencies).
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
@@ -16000,12 +16281,6 @@ paths:
|
||||
description: This endpoint helps to update metadata information like metric
|
||||
description, unit, type, temporality, monotonicity for a specified metric
|
||||
operationId: UpdateMetricMetadata
|
||||
parameters:
|
||||
- in: path
|
||||
name: metric_name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
@@ -16046,64 +16321,6 @@ paths:
|
||||
summary: Update metric metadata
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/inspect:
|
||||
post:
|
||||
deprecated: false
|
||||
description: Returns raw time series data points for a metric within a time
|
||||
range (max 30 minutes). Each series includes labels and timestamp/value pairs.
|
||||
operationId: InspectMetrics
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesInspectMetricsRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesInspectMetricsResponse'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Inspect raw metric data points
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/onboarding:
|
||||
get:
|
||||
deprecated: false
|
||||
|
||||
@@ -370,7 +370,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
|
||||
}
|
||||
|
||||
for _, cb := range provider.onBeforeRoleDelete {
|
||||
if err := cb(ctx, orgID, id, role.Name); err != nil {
|
||||
if err := cb(ctx, orgID, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,8 +90,12 @@ 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},
|
||||
Store: signoz.SQLStore,
|
||||
AgentFeatures: []agentConf.AgentFeature{
|
||||
logParsingPipelineController,
|
||||
signoz.Modules.SpanMapper,
|
||||
signoz.Modules.LLMPricingRule,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"test": "jest",
|
||||
"test:changedsince": "jest --changedSince=main --coverage --silent",
|
||||
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
|
||||
"generate:config:web-settings": "json2ts ../docs/config/web-settings.json -o src/types/generated/webSettings.ts --style.useTabs --style.tabWidth=1 --style.singleQuote --bannerComment '/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM docs/config/web-settings.json */'"
|
||||
"generate:config:web-settings": "json2ts ./src/schemas/generated/webSettings.schema.json -o src/types/generated/webSettings.ts --style.useTabs --style.tabWidth=1 --style.singleQuote --bannerComment '/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM frontend/src/schemas/generated/webSettings.schema.json */'"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
|
||||
@@ -4,14 +4,22 @@
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import { useMutation } from 'react-query';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
GetChecks200,
|
||||
GetChecksParams,
|
||||
InframonitoringtypesPostableClustersDTO,
|
||||
InframonitoringtypesPostableDaemonSetsDTO,
|
||||
InframonitoringtypesPostableDeploymentsDTO,
|
||||
@@ -39,7 +47,94 @@ import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports 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.
|
||||
* 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.
|
||||
* @summary List Clusters for Infra Monitoring
|
||||
*/
|
||||
export const listClusters = (
|
||||
@@ -122,7 +217,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 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.
|
||||
* 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.
|
||||
* @summary List DaemonSets for Infra Monitoring
|
||||
*/
|
||||
export const listDaemonSets = (
|
||||
@@ -205,7 +300,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 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.
|
||||
* 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.
|
||||
* @summary List Deployments for Infra Monitoring
|
||||
*/
|
||||
export const listDeployments = (
|
||||
@@ -288,7 +383,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 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.
|
||||
* 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.
|
||||
* @summary List Hosts for Infra Monitoring
|
||||
*/
|
||||
export const listHosts = (
|
||||
@@ -371,7 +466,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 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.
|
||||
* 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.
|
||||
* @summary List Jobs for Infra Monitoring
|
||||
*/
|
||||
export const listJobs = (
|
||||
@@ -454,7 +549,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 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.
|
||||
* 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.
|
||||
* @summary List Namespaces for Infra Monitoring
|
||||
*/
|
||||
export const listNamespaces = (
|
||||
@@ -537,7 +632,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 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.
|
||||
* 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.
|
||||
* @summary List Nodes for Infra Monitoring
|
||||
*/
|
||||
export const listNodes = (
|
||||
@@ -620,7 +715,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 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.
|
||||
* 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.
|
||||
* @summary List Pods for Infra Monitoring
|
||||
*/
|
||||
export const listPods = (
|
||||
@@ -703,7 +798,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 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.
|
||||
* 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.
|
||||
* @summary List Volumes for Infra Monitoring
|
||||
*/
|
||||
export const listVolumes = (
|
||||
@@ -786,7 +881,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 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.
|
||||
* 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.
|
||||
* @summary List StatefulSets for Infra Monitoring
|
||||
*/
|
||||
export const listStatefulSets = (
|
||||
|
||||
@@ -19,16 +19,15 @@ import type {
|
||||
|
||||
import type {
|
||||
GetMetricAlerts200,
|
||||
GetMetricAlertsPathParameters,
|
||||
GetMetricAlertsParams,
|
||||
GetMetricAttributes200,
|
||||
GetMetricAttributesParams,
|
||||
GetMetricAttributesPathParameters,
|
||||
GetMetricDashboards200,
|
||||
GetMetricDashboardsPathParameters,
|
||||
GetMetricDashboardsParams,
|
||||
GetMetricHighlights200,
|
||||
GetMetricHighlightsPathParameters,
|
||||
GetMetricHighlightsParams,
|
||||
GetMetricMetadata200,
|
||||
GetMetricMetadataPathParameters,
|
||||
GetMetricMetadataParams,
|
||||
GetMetricsOnboardingStatus200,
|
||||
GetMetricsStats200,
|
||||
GetMetricsTreemap200,
|
||||
@@ -40,7 +39,6 @@ import type {
|
||||
MetricsexplorertypesTreemapRequestDTO,
|
||||
MetricsexplorertypesUpdateMetricMetadataRequestDTO,
|
||||
RenderErrorResponseDTO,
|
||||
UpdateMetricMetadataPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
@@ -146,27 +144,26 @@ export const invalidateListMetrics = async (
|
||||
* @summary Get metric alerts
|
||||
*/
|
||||
export const getMetricAlerts = (
|
||||
{ metricName }: GetMetricAlertsPathParameters,
|
||||
params: GetMetricAlertsParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetMetricAlerts200>({
|
||||
url: `/api/v2/metrics/${metricName}/alerts`,
|
||||
url: `/api/v2/metrics/alerts`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetMetricAlertsQueryKey = ({
|
||||
metricName,
|
||||
}: GetMetricAlertsPathParameters) => {
|
||||
return [`/api/v2/metrics/${metricName}/alerts`] as const;
|
||||
export const getGetMetricAlertsQueryKey = (params?: GetMetricAlertsParams) => {
|
||||
return [`/api/v2/metrics/alerts`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getGetMetricAlertsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMetricAlerts>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ metricName }: GetMetricAlertsPathParameters,
|
||||
params: GetMetricAlertsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricAlerts>>,
|
||||
@@ -177,19 +174,13 @@ export const getGetMetricAlertsQueryOptions = <
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetMetricAlertsQueryKey({ metricName });
|
||||
const queryKey = queryOptions?.queryKey ?? getGetMetricAlertsQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getMetricAlerts>>> = ({
|
||||
signal,
|
||||
}) => getMetricAlerts({ metricName }, signal);
|
||||
}) => getMetricAlerts(params, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!metricName,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricAlerts>>,
|
||||
TError,
|
||||
TData
|
||||
@@ -209,7 +200,7 @@ export function useGetMetricAlerts<
|
||||
TData = Awaited<ReturnType<typeof getMetricAlerts>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ metricName }: GetMetricAlertsPathParameters,
|
||||
params: GetMetricAlertsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricAlerts>>,
|
||||
@@ -218,7 +209,7 @@ export function useGetMetricAlerts<
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetMetricAlertsQueryOptions({ metricName }, options);
|
||||
const queryOptions = getGetMetricAlertsQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
@@ -232,11 +223,11 @@ export function useGetMetricAlerts<
|
||||
*/
|
||||
export const invalidateGetMetricAlerts = async (
|
||||
queryClient: QueryClient,
|
||||
{ metricName }: GetMetricAlertsPathParameters,
|
||||
params: GetMetricAlertsParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetMetricAlertsQueryKey({ metricName }) },
|
||||
{ queryKey: getGetMetricAlertsQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
@@ -248,12 +239,11 @@ export const invalidateGetMetricAlerts = async (
|
||||
* @summary Get metric attributes
|
||||
*/
|
||||
export const getMetricAttributes = (
|
||||
{ metricName }: GetMetricAttributesPathParameters,
|
||||
params?: GetMetricAttributesParams,
|
||||
params: GetMetricAttributesParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetMetricAttributes200>({
|
||||
url: `/api/v2/metrics/${metricName}/attributes`,
|
||||
url: `/api/v2/metrics/attributes`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
@@ -261,21 +251,16 @@ export const getMetricAttributes = (
|
||||
};
|
||||
|
||||
export const getGetMetricAttributesQueryKey = (
|
||||
{ metricName }: GetMetricAttributesPathParameters,
|
||||
params?: GetMetricAttributesParams,
|
||||
) => {
|
||||
return [
|
||||
`/api/v2/metrics/${metricName}/attributes`,
|
||||
...(params ? [params] : []),
|
||||
] as const;
|
||||
return [`/api/v2/metrics/attributes`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getGetMetricAttributesQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMetricAttributes>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ metricName }: GetMetricAttributesPathParameters,
|
||||
params?: GetMetricAttributesParams,
|
||||
params: GetMetricAttributesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricAttributes>>,
|
||||
@@ -287,19 +272,13 @@ export const getGetMetricAttributesQueryOptions = <
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ??
|
||||
getGetMetricAttributesQueryKey({ metricName }, params);
|
||||
queryOptions?.queryKey ?? getGetMetricAttributesQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getMetricAttributes>>
|
||||
> = ({ signal }) => getMetricAttributes({ metricName }, params, signal);
|
||||
> = ({ signal }) => getMetricAttributes(params, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!metricName,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricAttributes>>,
|
||||
TError,
|
||||
TData
|
||||
@@ -319,8 +298,7 @@ export function useGetMetricAttributes<
|
||||
TData = Awaited<ReturnType<typeof getMetricAttributes>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ metricName }: GetMetricAttributesPathParameters,
|
||||
params?: GetMetricAttributesParams,
|
||||
params: GetMetricAttributesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricAttributes>>,
|
||||
@@ -329,11 +307,7 @@ export function useGetMetricAttributes<
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetMetricAttributesQueryOptions(
|
||||
{ metricName },
|
||||
params,
|
||||
options,
|
||||
);
|
||||
const queryOptions = getGetMetricAttributesQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
@@ -347,12 +321,11 @@ export function useGetMetricAttributes<
|
||||
*/
|
||||
export const invalidateGetMetricAttributes = async (
|
||||
queryClient: QueryClient,
|
||||
{ metricName }: GetMetricAttributesPathParameters,
|
||||
params?: GetMetricAttributesParams,
|
||||
params: GetMetricAttributesParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetMetricAttributesQueryKey({ metricName }, params) },
|
||||
{ queryKey: getGetMetricAttributesQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
@@ -364,27 +337,28 @@ export const invalidateGetMetricAttributes = async (
|
||||
* @summary Get metric dashboards
|
||||
*/
|
||||
export const getMetricDashboards = (
|
||||
{ metricName }: GetMetricDashboardsPathParameters,
|
||||
params: GetMetricDashboardsParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetMetricDashboards200>({
|
||||
url: `/api/v2/metrics/${metricName}/dashboards`,
|
||||
url: `/api/v2/metrics/dashboards`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetMetricDashboardsQueryKey = ({
|
||||
metricName,
|
||||
}: GetMetricDashboardsPathParameters) => {
|
||||
return [`/api/v2/metrics/${metricName}/dashboards`] as const;
|
||||
export const getGetMetricDashboardsQueryKey = (
|
||||
params?: GetMetricDashboardsParams,
|
||||
) => {
|
||||
return [`/api/v2/metrics/dashboards`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getGetMetricDashboardsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ metricName }: GetMetricDashboardsPathParameters,
|
||||
params: GetMetricDashboardsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricDashboards>>,
|
||||
@@ -396,18 +370,13 @@ export const getGetMetricDashboardsQueryOptions = <
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetMetricDashboardsQueryKey({ metricName });
|
||||
queryOptions?.queryKey ?? getGetMetricDashboardsQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getMetricDashboards>>
|
||||
> = ({ signal }) => getMetricDashboards({ metricName }, signal);
|
||||
> = ({ signal }) => getMetricDashboards(params, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!metricName,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricDashboards>>,
|
||||
TError,
|
||||
TData
|
||||
@@ -427,7 +396,7 @@ export function useGetMetricDashboards<
|
||||
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ metricName }: GetMetricDashboardsPathParameters,
|
||||
params: GetMetricDashboardsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricDashboards>>,
|
||||
@@ -436,10 +405,7 @@ export function useGetMetricDashboards<
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetMetricDashboardsQueryOptions(
|
||||
{ metricName },
|
||||
options,
|
||||
);
|
||||
const queryOptions = getGetMetricDashboardsQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
@@ -453,11 +419,11 @@ export function useGetMetricDashboards<
|
||||
*/
|
||||
export const invalidateGetMetricDashboards = async (
|
||||
queryClient: QueryClient,
|
||||
{ metricName }: GetMetricDashboardsPathParameters,
|
||||
params: GetMetricDashboardsParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetMetricDashboardsQueryKey({ metricName }) },
|
||||
{ queryKey: getGetMetricDashboardsQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
@@ -469,27 +435,28 @@ export const invalidateGetMetricDashboards = async (
|
||||
* @summary Get metric highlights
|
||||
*/
|
||||
export const getMetricHighlights = (
|
||||
{ metricName }: GetMetricHighlightsPathParameters,
|
||||
params: GetMetricHighlightsParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetMetricHighlights200>({
|
||||
url: `/api/v2/metrics/${metricName}/highlights`,
|
||||
url: `/api/v2/metrics/highlights`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetMetricHighlightsQueryKey = ({
|
||||
metricName,
|
||||
}: GetMetricHighlightsPathParameters) => {
|
||||
return [`/api/v2/metrics/${metricName}/highlights`] as const;
|
||||
export const getGetMetricHighlightsQueryKey = (
|
||||
params?: GetMetricHighlightsParams,
|
||||
) => {
|
||||
return [`/api/v2/metrics/highlights`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getGetMetricHighlightsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMetricHighlights>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ metricName }: GetMetricHighlightsPathParameters,
|
||||
params: GetMetricHighlightsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricHighlights>>,
|
||||
@@ -501,18 +468,13 @@ export const getGetMetricHighlightsQueryOptions = <
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetMetricHighlightsQueryKey({ metricName });
|
||||
queryOptions?.queryKey ?? getGetMetricHighlightsQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getMetricHighlights>>
|
||||
> = ({ signal }) => getMetricHighlights({ metricName }, signal);
|
||||
> = ({ signal }) => getMetricHighlights(params, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!metricName,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricHighlights>>,
|
||||
TError,
|
||||
TData
|
||||
@@ -532,7 +494,7 @@ export function useGetMetricHighlights<
|
||||
TData = Awaited<ReturnType<typeof getMetricHighlights>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ metricName }: GetMetricHighlightsPathParameters,
|
||||
params: GetMetricHighlightsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricHighlights>>,
|
||||
@@ -541,10 +503,7 @@ export function useGetMetricHighlights<
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetMetricHighlightsQueryOptions(
|
||||
{ metricName },
|
||||
options,
|
||||
);
|
||||
const queryOptions = getGetMetricHighlightsQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
@@ -558,219 +517,17 @@ export function useGetMetricHighlights<
|
||||
*/
|
||||
export const invalidateGetMetricHighlights = async (
|
||||
queryClient: QueryClient,
|
||||
{ metricName }: GetMetricHighlightsPathParameters,
|
||||
params: GetMetricHighlightsParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetMetricHighlightsQueryKey({ metricName }) },
|
||||
{ queryKey: getGetMetricHighlightsQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint returns metadata information like metric description, unit, type, temporality, monotonicity for a specified metric
|
||||
* @summary Get metric metadata
|
||||
*/
|
||||
export const getMetricMetadata = (
|
||||
{ metricName }: GetMetricMetadataPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetMetricMetadata200>({
|
||||
url: `/api/v2/metrics/${metricName}/metadata`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetMetricMetadataQueryKey = ({
|
||||
metricName,
|
||||
}: GetMetricMetadataPathParameters) => {
|
||||
return [`/api/v2/metrics/${metricName}/metadata`] as const;
|
||||
};
|
||||
|
||||
export const getGetMetricMetadataQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ metricName }: GetMetricMetadataPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricMetadata>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetMetricMetadataQueryKey({ metricName });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getMetricMetadata>>
|
||||
> = ({ signal }) => getMetricMetadata({ metricName }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!metricName,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricMetadata>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetMetricMetadataQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getMetricMetadata>>
|
||||
>;
|
||||
export type GetMetricMetadataQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get metric metadata
|
||||
*/
|
||||
|
||||
export function useGetMetricMetadata<
|
||||
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ metricName }: GetMetricMetadataPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricMetadata>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetMetricMetadataQueryOptions({ metricName }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get metric metadata
|
||||
*/
|
||||
export const invalidateGetMetricMetadata = async (
|
||||
queryClient: QueryClient,
|
||||
{ metricName }: GetMetricMetadataPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetMetricMetadataQueryKey({ metricName }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint helps to update metadata information like metric description, unit, type, temporality, monotonicity for a specified metric
|
||||
* @summary Update metric metadata
|
||||
*/
|
||||
export const updateMetricMetadata = (
|
||||
{ metricName }: UpdateMetricMetadataPathParameters,
|
||||
metricsexplorertypesUpdateMetricMetadataRequestDTO?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v2/metrics/${metricName}/metadata`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: metricsexplorertypesUpdateMetricMetadataRequestDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateMetricMetadataMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMetricMetadata>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateMetricMetadataPathParameters;
|
||||
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMetricMetadata>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateMetricMetadataPathParameters;
|
||||
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateMetricMetadata'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof updateMetricMetadata>>,
|
||||
{
|
||||
pathParams: UpdateMetricMetadataPathParameters;
|
||||
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateMetricMetadata(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateMetricMetadataMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateMetricMetadata>>
|
||||
>;
|
||||
export type UpdateMetricMetadataMutationBody =
|
||||
| BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>
|
||||
| undefined;
|
||||
export type UpdateMetricMetadataMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update metric metadata
|
||||
*/
|
||||
export const useUpdateMetricMetadata = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMetricMetadata>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateMetricMetadataPathParameters;
|
||||
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateMetricMetadata>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateMetricMetadataPathParameters;
|
||||
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateMetricMetadataMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Returns raw time series data points for a metric within a time range (max 30 minutes). Each series includes labels and timestamp/value pairs.
|
||||
* @summary Inspect raw metric data points
|
||||
@@ -854,6 +611,188 @@ export const useInspectMetrics = <
|
||||
> => {
|
||||
return useMutation(getInspectMetricsMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint returns metadata information like metric description, unit, type, temporality, monotonicity for a specified metric
|
||||
* @summary Get metric metadata
|
||||
*/
|
||||
export const getMetricMetadata = (
|
||||
params: GetMetricMetadataParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetMetricMetadata200>({
|
||||
url: `/api/v2/metrics/metadata`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetMetricMetadataQueryKey = (
|
||||
params?: GetMetricMetadataParams,
|
||||
) => {
|
||||
return [`/api/v2/metrics/metadata`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getGetMetricMetadataQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: GetMetricMetadataParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricMetadata>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetMetricMetadataQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getMetricMetadata>>
|
||||
> = ({ signal }) => getMetricMetadata(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricMetadata>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetMetricMetadataQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getMetricMetadata>>
|
||||
>;
|
||||
export type GetMetricMetadataQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get metric metadata
|
||||
*/
|
||||
|
||||
export function useGetMetricMetadata<
|
||||
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: GetMetricMetadataParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricMetadata>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetMetricMetadataQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get metric metadata
|
||||
*/
|
||||
export const invalidateGetMetricMetadata = async (
|
||||
queryClient: QueryClient,
|
||||
params: GetMetricMetadataParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetMetricMetadataQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint helps to update metadata information like metric description, unit, type, temporality, monotonicity for a specified metric
|
||||
* @summary Update metric metadata
|
||||
*/
|
||||
export const updateMetricMetadata = (
|
||||
metricsexplorertypesUpdateMetricMetadataRequestDTO?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v2/metrics/metadata`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: metricsexplorertypesUpdateMetricMetadataRequestDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateMetricMetadataMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMetricMetadata>>,
|
||||
TError,
|
||||
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMetricMetadata>>,
|
||||
TError,
|
||||
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateMetricMetadata'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof updateMetricMetadata>>,
|
||||
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return updateMetricMetadata(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateMetricMetadataMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateMetricMetadata>>
|
||||
>;
|
||||
export type UpdateMetricMetadataMutationBody =
|
||||
| BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>
|
||||
| undefined;
|
||||
export type UpdateMetricMetadataMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update metric metadata
|
||||
*/
|
||||
export const useUpdateMetricMetadata = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMetricMetadata>>,
|
||||
TError,
|
||||
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateMetricMetadata>>,
|
||||
TError,
|
||||
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateMetricMetadataMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Lightweight endpoint that checks if any non-SigNoz metrics have been ingested, used for onboarding status detection
|
||||
* @summary Check if non-SigNoz metrics have been received
|
||||
|
||||
@@ -2094,6 +2094,45 @@ export interface AuthtypesGettableTokenDTO {
|
||||
tokenType?: string;
|
||||
}
|
||||
|
||||
export enum CoretypesKindDTO {
|
||||
anonymous = 'anonymous',
|
||||
organization = 'organization',
|
||||
role = 'role',
|
||||
serviceaccount = 'serviceaccount',
|
||||
user = 'user',
|
||||
'notification-channel' = 'notification-channel',
|
||||
'route-policy' = 'route-policy',
|
||||
'apdex-setting' = 'apdex-setting',
|
||||
'auth-domain' = 'auth-domain',
|
||||
session = 'session',
|
||||
'cloud-integration' = 'cloud-integration',
|
||||
'cloud-integration-service' = 'cloud-integration-service',
|
||||
integration = 'integration',
|
||||
dashboard = 'dashboard',
|
||||
'public-dashboard' = 'public-dashboard',
|
||||
'ingestion-key' = 'ingestion-key',
|
||||
'ingestion-limit' = 'ingestion-limit',
|
||||
pipeline = 'pipeline',
|
||||
'user-preference' = 'user-preference',
|
||||
'org-preference' = 'org-preference',
|
||||
'quick-filter' = 'quick-filter',
|
||||
'ttl-setting' = 'ttl-setting',
|
||||
rule = 'rule',
|
||||
'planned-maintenance' = 'planned-maintenance',
|
||||
'saved-view' = 'saved-view',
|
||||
'trace-funnel' = 'trace-funnel',
|
||||
'factor-password' = 'factor-password',
|
||||
'factor-api-key' = 'factor-api-key',
|
||||
license = 'license',
|
||||
subscription = 'subscription',
|
||||
logs = 'logs',
|
||||
traces = 'traces',
|
||||
metrics = 'metrics',
|
||||
'audit-logs' = 'audit-logs',
|
||||
'meter-metrics' = 'meter-metrics',
|
||||
'logs-field' = 'logs-field',
|
||||
'traces-field' = 'traces-field',
|
||||
}
|
||||
export enum CoretypesTypeDTO {
|
||||
user = 'user',
|
||||
serviceaccount = 'serviceaccount',
|
||||
@@ -2104,10 +2143,7 @@ export enum CoretypesTypeDTO {
|
||||
telemetryresource = 'telemetryresource',
|
||||
}
|
||||
export interface CoretypesResourceRefDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
kind: string;
|
||||
kind: CoretypesKindDTO;
|
||||
type: CoretypesTypeDTO;
|
||||
}
|
||||
|
||||
@@ -2243,12 +2279,12 @@ export interface AuthtypesPostableRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description: string;
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
transactionGroups: AuthtypesTransactionGroupsDTO;
|
||||
transactionGroups?: AuthtypesTransactionGroupsDTO;
|
||||
}
|
||||
|
||||
export interface AuthtypesPostableRotateTokenDTO {
|
||||
@@ -5422,6 +5458,121 @@ 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;
|
||||
};
|
||||
@@ -5499,13 +5650,6 @@ export interface InframonitoringtypesClusterRecordDTO {
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesRequiredMetricsCheckDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
missingMetrics: string[] | null;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesResponseTypeDTO {
|
||||
list = 'list',
|
||||
grouped_list = 'grouped_list',
|
||||
@@ -5541,7 +5685,6 @@ export interface InframonitoringtypesClustersDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesClusterRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -5619,7 +5762,6 @@ export interface InframonitoringtypesDaemonSetsDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesDaemonSetRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -5697,7 +5839,6 @@ export interface InframonitoringtypesDeploymentsDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesDeploymentRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -5783,7 +5924,6 @@ export interface InframonitoringtypesHostsDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesHostRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -5869,7 +6009,6 @@ export interface InframonitoringtypesJobsDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesJobRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -5919,7 +6058,6 @@ export interface InframonitoringtypesNamespacesDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesNamespaceRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -5986,7 +6124,6 @@ export interface InframonitoringtypesNodesDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesNodeRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -6070,7 +6207,6 @@ export interface InframonitoringtypesPodsDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesPodRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -6418,7 +6554,6 @@ export interface InframonitoringtypesStatefulSetsDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesStatefulSetRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -6487,7 +6622,6 @@ export interface InframonitoringtypesVolumesDTO {
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesVolumeRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -10210,6 +10344,21 @@ 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;
|
||||
/**
|
||||
@@ -10334,9 +10483,14 @@ export type ListMetrics200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMetricAlertsPathParameters = {
|
||||
export type GetMetricAlertsParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
|
||||
*/
|
||||
metricName: string;
|
||||
};
|
||||
|
||||
export type GetMetricAlerts200 = {
|
||||
data: MetricsexplorertypesMetricAlertsResponseDTO;
|
||||
/**
|
||||
@@ -10345,18 +10499,20 @@ export type GetMetricAlerts200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMetricAttributesPathParameters = {
|
||||
metricName: string;
|
||||
};
|
||||
export type GetMetricAttributesParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
|
||||
*/
|
||||
metricName: string;
|
||||
/**
|
||||
* @type integer,null
|
||||
* @description undefined
|
||||
* @description Start of the time range as a Unix timestamp in milliseconds.
|
||||
*/
|
||||
start?: number | null;
|
||||
/**
|
||||
* @type integer,null
|
||||
* @description undefined
|
||||
* @description End of the time range as a Unix timestamp in milliseconds.
|
||||
*/
|
||||
end?: number | null;
|
||||
};
|
||||
@@ -10369,9 +10525,14 @@ export type GetMetricAttributes200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMetricDashboardsPathParameters = {
|
||||
export type GetMetricDashboardsParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
|
||||
*/
|
||||
metricName: string;
|
||||
};
|
||||
|
||||
export type GetMetricDashboards200 = {
|
||||
data: MetricsexplorertypesMetricDashboardsResponseDTO;
|
||||
/**
|
||||
@@ -10380,9 +10541,14 @@ export type GetMetricDashboards200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMetricHighlightsPathParameters = {
|
||||
export type GetMetricHighlightsParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
|
||||
*/
|
||||
metricName: string;
|
||||
};
|
||||
|
||||
export type GetMetricHighlights200 = {
|
||||
data: MetricsexplorertypesMetricHighlightsResponseDTO;
|
||||
/**
|
||||
@@ -10391,22 +10557,24 @@ export type GetMetricHighlights200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMetricMetadataPathParameters = {
|
||||
metricName: string;
|
||||
};
|
||||
export type GetMetricMetadata200 = {
|
||||
data: MetricsexplorertypesMetricMetadataDTO;
|
||||
export type InspectMetrics200 = {
|
||||
data: MetricsexplorertypesInspectMetricsResponseDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateMetricMetadataPathParameters = {
|
||||
export type GetMetricMetadataParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
|
||||
*/
|
||||
metricName: string;
|
||||
};
|
||||
export type InspectMetrics200 = {
|
||||
data: MetricsexplorertypesInspectMetricsResponseDTO;
|
||||
|
||||
export type GetMetricMetadata200 = {
|
||||
data: MetricsexplorertypesMetricMetadataDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
@@ -191,9 +191,6 @@ function TimeSeries({
|
||||
if (metrics[0] && yAxisUnit) {
|
||||
updateMetricMetadata(
|
||||
{
|
||||
pathParams: {
|
||||
metricName: metricNames[0],
|
||||
},
|
||||
data: buildUpdateMetricYAxisUnitPayload(
|
||||
metricNames[0],
|
||||
metrics[0],
|
||||
|
||||
@@ -48,18 +48,14 @@ function AllAttributes({
|
||||
isLoading: isLoadingAttributes,
|
||||
isError: isErrorAttributes,
|
||||
refetch: refetchAttributes,
|
||||
} = useGetMetricAttributes(
|
||||
{
|
||||
metricName,
|
||||
},
|
||||
{
|
||||
start: minTime ? Math.floor(minTime / 1000000) : undefined,
|
||||
end: maxTime ? Math.floor(maxTime / 1000000) : undefined,
|
||||
},
|
||||
);
|
||||
} = useGetMetricAttributes({
|
||||
metricName,
|
||||
start: minTime ? Math.floor(minTime / 1000000) : undefined,
|
||||
end: maxTime ? Math.floor(maxTime / 1000000) : undefined,
|
||||
});
|
||||
|
||||
const attributes = useMemo(
|
||||
() => attributesData?.data?.attributes ?? [],
|
||||
() => attributesData?.data.attributes ?? [],
|
||||
[attributesData],
|
||||
);
|
||||
|
||||
|
||||
@@ -237,9 +237,6 @@ function Metadata({
|
||||
const handleSave = useCallback(() => {
|
||||
updateMetricMetadata(
|
||||
{
|
||||
pathParams: {
|
||||
metricName,
|
||||
},
|
||||
data: transformUpdateMetricMetadataRequest(metricName, metricMetadataState),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -56,7 +56,7 @@ function MetricDetails({
|
||||
);
|
||||
|
||||
const metadata = useMemo(() => {
|
||||
if (!metricMetadataResponse?.data) {
|
||||
if (!metricMetadataResponse) {
|
||||
return null;
|
||||
}
|
||||
const { type, description, unit, temporality, isMonotonic } =
|
||||
|
||||
@@ -195,14 +195,12 @@ describe('Metadata', () => {
|
||||
expect(mockUseUpdateMetricMetadata).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
metricName: MOCK_METRIC_NAME,
|
||||
type: MetrictypesTypeDTO.sum,
|
||||
temporality: MetrictypesTemporalityDTO.cumulative,
|
||||
unit: 'By',
|
||||
isMonotonic: true,
|
||||
}),
|
||||
pathParams: {
|
||||
metricName: MOCK_METRIC_NAME,
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
CoretypesKindDTO,
|
||||
CoretypesObjectGroupDTO,
|
||||
CoretypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
@@ -56,7 +57,7 @@ const baseAuthzResources: AuthzResources = {
|
||||
|
||||
// API payload resource refs — only kind+type, no allowedVerbs (matches CoretypesResourceRefDTO shape)
|
||||
const dashboardResourceRef = {
|
||||
kind: 'dashboard',
|
||||
kind: 'dashboard' as CoretypesKindDTO,
|
||||
type: 'metaresource' as CoretypesTypeDTO,
|
||||
};
|
||||
const alertResourceRef = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import type {
|
||||
CoretypesKindDTO,
|
||||
CoretypesObjectGroupDTO,
|
||||
CoretypesResourceRefDTO,
|
||||
CoretypesTypeDTO,
|
||||
@@ -147,7 +148,7 @@ export function buildPatchPayload({
|
||||
continue;
|
||||
}
|
||||
const resourceDef: CoretypesResourceRefDTO = {
|
||||
kind: found.kind,
|
||||
kind: found.kind as CoretypesKindDTO,
|
||||
type: found.type as CoretypesTypeDTO,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ import {
|
||||
AuthtypesTransactionDTO,
|
||||
CoretypesTypeDTO,
|
||||
AuthtypesRelationDTO,
|
||||
CoretypesKindDTO,
|
||||
} from '../../api/generated/services/sigNoz.schemas';
|
||||
import permissionsType from './permissions.type';
|
||||
import {
|
||||
AuthZObject,
|
||||
AuthZRelation,
|
||||
BrandedPermission,
|
||||
ResourceName,
|
||||
ResourcesForRelation,
|
||||
ResourceType,
|
||||
} from './types';
|
||||
@@ -87,7 +87,7 @@ export function permissionToTransactionDto(
|
||||
relation: relation as AuthtypesRelationDTO,
|
||||
object: {
|
||||
resource: {
|
||||
kind: resourceName as ResourceName,
|
||||
kind: resourceName as CoretypesKindDTO,
|
||||
type: type as CoretypesTypeDTO,
|
||||
},
|
||||
selector: selector || '*',
|
||||
|
||||
117
frontend/src/hooks/useInlineOverflowCount.ts
Normal file
117
frontend/src/hooks/useInlineOverflowCount.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface UseInlineOverflowCountOptions {
|
||||
itemCount: number;
|
||||
/** Horizontal gap between items, in px. */
|
||||
gap?: number;
|
||||
/** Width kept free at the end of the line for a trailing "+N" trigger, in px. */
|
||||
reserveWidth?: number;
|
||||
/** Pause measuring (e.g. while expanded) without unmounting. */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface UseInlineOverflowCountResult {
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
visibleCount: number;
|
||||
overflowCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures how many of a container's children (each marked
|
||||
* `data-overflow-item="true"`) fit on a single line, reserving `reserveWidth`
|
||||
* for a trailing "+N" trigger. Item widths are cached, so children hidden with
|
||||
* `display: none` still count toward the fit; measuring pauses while `enabled`
|
||||
* is false.
|
||||
*/
|
||||
export function useInlineOverflowCount({
|
||||
itemCount,
|
||||
gap = 8,
|
||||
reserveWidth = 0,
|
||||
enabled = true,
|
||||
}: UseInlineOverflowCountOptions): UseInlineOverflowCountResult {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [visibleCount, setVisibleCount] = useState(itemCount);
|
||||
const itemWidthsRef = useRef<number[]>([]);
|
||||
const enabledRef = useRef(enabled);
|
||||
enabledRef.current = enabled;
|
||||
|
||||
useEffect(() => {
|
||||
itemWidthsRef.current = [];
|
||||
setVisibleCount(itemCount);
|
||||
}, [itemCount]);
|
||||
|
||||
const measure = useCallback((): void => {
|
||||
const container = containerRef.current;
|
||||
if (!container || !enabledRef.current) {
|
||||
return;
|
||||
}
|
||||
const itemElements = Array.from(container.children).filter(
|
||||
(itemElement): itemElement is HTMLElement =>
|
||||
itemElement instanceof HTMLElement &&
|
||||
itemElement.dataset.overflowItem === 'true',
|
||||
);
|
||||
if (itemElements.length === 0) {
|
||||
setVisibleCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
itemElements.forEach((itemElement, index) => {
|
||||
if (itemElement.offsetWidth > 0) {
|
||||
itemWidthsRef.current[index] = itemElement.offsetWidth;
|
||||
}
|
||||
});
|
||||
const cachedWidths: number[] = [];
|
||||
for (let index = 0; index < itemElements.length; index += 1) {
|
||||
const cachedWidth = itemWidthsRef.current[index];
|
||||
if (cachedWidth == null) {
|
||||
// Width not cached yet — reveal everything for one frame so it gets
|
||||
// measured, then the next pass collapses accurately.
|
||||
setVisibleCount(itemElements.length);
|
||||
return;
|
||||
}
|
||||
cachedWidths.push(cachedWidth);
|
||||
}
|
||||
|
||||
const containerWidth = container.clientWidth;
|
||||
const totalWidth = cachedWidths.reduce(
|
||||
(runningTotal, itemWidth, index) =>
|
||||
runningTotal + itemWidth + (index > 0 ? gap : 0),
|
||||
0,
|
||||
);
|
||||
if (totalWidth <= containerWidth) {
|
||||
setVisibleCount(itemElements.length);
|
||||
return;
|
||||
}
|
||||
|
||||
const availableWidth = containerWidth - reserveWidth;
|
||||
let usedWidth = 0;
|
||||
let fitCount = 0;
|
||||
for (let index = 0; index < cachedWidths.length; index += 1) {
|
||||
const itemWidthWithGap = cachedWidths[index] + (index > 0 ? gap : 0);
|
||||
if (usedWidth + itemWidthWithGap > availableWidth && fitCount > 0) {
|
||||
break;
|
||||
}
|
||||
usedWidth += itemWidthWithGap;
|
||||
fitCount += 1;
|
||||
}
|
||||
setVisibleCount(Math.max(1, fitCount));
|
||||
}, [gap, reserveWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) {
|
||||
return undefined;
|
||||
}
|
||||
const observer = new ResizeObserver(() => measure());
|
||||
observer.observe(container);
|
||||
Array.from(container.children).forEach((child) => observer.observe(child));
|
||||
measure();
|
||||
return (): void => observer.disconnect();
|
||||
}, [measure, itemCount, enabled]);
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
visibleCount,
|
||||
overflowCount: Math.max(0, itemCount - visibleCount),
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
.dashboardActionsContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dashboardActionsSecondary {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FullScreenHandle } from 'react-full-screen';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import {
|
||||
Braces,
|
||||
ClipboardCopy,
|
||||
Configure,
|
||||
Ellipsis,
|
||||
Copy,
|
||||
FileJson,
|
||||
Fullscreen,
|
||||
Grid3X3,
|
||||
LockKeyhole,
|
||||
PenLine,
|
||||
Plus,
|
||||
SquareStack,
|
||||
Trash2,
|
||||
} from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||
import type { MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { cloneDashboardV2 } from 'api/generated/services/dashboard';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import ROUTES from 'constants/routes';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import ConfirmDeleteDialog from '../../components/ConfirmDeleteDialog/ConfirmDeleteDialog';
|
||||
import DashboardSettings from '../../DashboardSettings';
|
||||
import { useAddSection } from '../../PanelsAndSectionsLayout/Section/hooks/useAddSection';
|
||||
import SectionTitleModal from '../../PanelsAndSectionsLayout/Section/SectionTitleModal';
|
||||
import JsonEditorDrawer from '../JsonEditorDrawer/JsonEditorDrawer';
|
||||
import SettingsDrawer from '../SettingsDrawer';
|
||||
import styles from './DashboardActions.module.scss';
|
||||
import { useDashboardStore } from '../../store/useDashboardStore';
|
||||
@@ -55,14 +65,31 @@ function DashboardActions({
|
||||
const canEdit = useDashboardStore((s) => s.isEditable);
|
||||
const { user } = useAppContext();
|
||||
const { t } = useTranslation(['dashboard', 'common']);
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] =
|
||||
useState<boolean>(false);
|
||||
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState<boolean>(false);
|
||||
const [isCloning, setIsCloning] = useState<boolean>(false);
|
||||
const [isNewSectionOpen, setIsNewSectionOpen] = useState<boolean>(false);
|
||||
|
||||
const [state, setCopy] = useCopyToClipboard();
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState<boolean>(false);
|
||||
const deleteDashboardMutation = useDeleteDashboard(dashboard.id);
|
||||
|
||||
const { addSection, isSaving: isAddingSection } = useAddSection({
|
||||
layouts: dashboard.spec.layouts,
|
||||
});
|
||||
|
||||
const handleCreateSection = useCallback(
|
||||
async (title: string): Promise<void> => {
|
||||
await addSection(title);
|
||||
setIsNewSectionOpen(false);
|
||||
},
|
||||
[addSection],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.error) {
|
||||
toast.error(t('something_went_wrong', { ns: 'common' }));
|
||||
@@ -89,6 +116,24 @@ function DashboardActions({
|
||||
URL.revokeObjectURL(url);
|
||||
}, [dashboardDataJSON, title]);
|
||||
|
||||
const handleClone = useCallback(async (): Promise<void> => {
|
||||
if (!dashboard.id) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsCloning(true);
|
||||
const response = await cloneDashboardV2({ id: dashboard.id });
|
||||
toast.success('Dashboard cloned');
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, { dashboardId: response.data.id }),
|
||||
);
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
} finally {
|
||||
setIsCloning(false);
|
||||
}
|
||||
}, [dashboard.id, safeNavigate, showErrorModal]);
|
||||
|
||||
const handleConfirmDelete = useCallback((): void => {
|
||||
deleteDashboardMutation.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
@@ -99,17 +144,24 @@ function DashboardActions({
|
||||
}, [deleteDashboardMutation]);
|
||||
|
||||
const menuItems = useMemo<MenuItem[]>(() => {
|
||||
const editGroup: MenuItem[] = [];
|
||||
const dashboardGroup: MenuItem[] = [];
|
||||
if (canEdit) {
|
||||
editGroup.push({
|
||||
dashboardGroup.push({
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
icon: <PenLine size={14} />,
|
||||
onClick: onOpenRename,
|
||||
});
|
||||
}
|
||||
dashboardGroup.push({
|
||||
key: 'clone',
|
||||
label: 'Clone dashboard',
|
||||
icon: <Copy size={14} />,
|
||||
disabled: isCloning,
|
||||
onClick: (): void => void handleClone(),
|
||||
});
|
||||
if (isAuthor || user.role === USER_ROLES.ADMIN) {
|
||||
editGroup.push({
|
||||
dashboardGroup.push({
|
||||
key: 'lock',
|
||||
label: isDashboardLocked ? 'Unlock dashboard' : 'Lock dashboard',
|
||||
icon: <LockKeyhole size={14} />,
|
||||
@@ -117,14 +169,14 @@ function DashboardActions({
|
||||
onClick: onLockToggle,
|
||||
});
|
||||
}
|
||||
editGroup.push({
|
||||
dashboardGroup.push({
|
||||
key: 'fullscreen',
|
||||
label: 'Full screen',
|
||||
icon: <Fullscreen size={14} />,
|
||||
onClick: handle.enter,
|
||||
});
|
||||
|
||||
const exportGroup: MenuItem[] = [
|
||||
const dataGroup: MenuItem[] = [
|
||||
{
|
||||
key: 'export',
|
||||
label: 'Export JSON',
|
||||
@@ -139,7 +191,35 @@ function DashboardActions({
|
||||
},
|
||||
];
|
||||
|
||||
const dangerGroup: MenuItem[] = [
|
||||
const layoutGroup: MenuItem[] = [];
|
||||
if (canEdit) {
|
||||
layoutGroup.push({
|
||||
key: 'new-section',
|
||||
label: 'New section',
|
||||
icon: <SquareStack size={14} />,
|
||||
onClick: (): void => setIsNewSectionOpen(true),
|
||||
});
|
||||
}
|
||||
|
||||
const items: MenuItem[] = [
|
||||
{
|
||||
type: 'group',
|
||||
key: 'group-dashboard',
|
||||
label: 'Dashboard',
|
||||
children: dashboardGroup,
|
||||
},
|
||||
{ type: 'group', key: 'group-data', label: 'Data', children: dataGroup },
|
||||
];
|
||||
if (layoutGroup.length > 0) {
|
||||
items.push({
|
||||
type: 'group',
|
||||
key: 'group-layout',
|
||||
label: 'Layout',
|
||||
children: layoutGroup,
|
||||
});
|
||||
}
|
||||
items.push(
|
||||
{ type: 'divider', key: 'divider-danger' },
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete dashboard',
|
||||
@@ -147,74 +227,85 @@ function DashboardActions({
|
||||
danger: true,
|
||||
onClick: (): void => setIsDeleteOpen(true),
|
||||
},
|
||||
];
|
||||
|
||||
return [editGroup, exportGroup, dangerGroup]
|
||||
.filter((group) => group.length > 0)
|
||||
.flatMap((group, index) =>
|
||||
index > 0 ? [{ type: 'divider' } as MenuItem, ...group] : group,
|
||||
);
|
||||
);
|
||||
return items;
|
||||
}, [
|
||||
isDashboardLocked,
|
||||
canEdit,
|
||||
isCloning,
|
||||
isAuthor,
|
||||
user.role,
|
||||
isDashboardLocked,
|
||||
dashboard.createdBy,
|
||||
onOpenRename,
|
||||
handleClone,
|
||||
onLockToggle,
|
||||
handle.enter,
|
||||
exportJSON,
|
||||
setCopy,
|
||||
dashboardDataJSON,
|
||||
canEdit,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={styles.dashboardActionsContainer}>
|
||||
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
|
||||
<div className={styles.dashboardActionsSecondary}>
|
||||
<DropdownMenuSimple menu={{ items: menuItems }}>
|
||||
<DropdownMenuSimple menu={{ items: menuItems }}>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="md"
|
||||
prefix={<Grid3X3 size="md" />}
|
||||
testId="options"
|
||||
>
|
||||
Actions
|
||||
</Button>
|
||||
</DropdownMenuSimple>
|
||||
{canEdit && (
|
||||
<>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
prefix={<Ellipsis size="md" />}
|
||||
testId="options"
|
||||
/>
|
||||
</DropdownMenuSimple>
|
||||
{canEdit && (
|
||||
<>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
prefix={<Configure size="md" />}
|
||||
testId="show-drawer"
|
||||
onClick={(): void => setIsSettingsDrawerOpen(true)}
|
||||
size="md"
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
<SettingsDrawer
|
||||
drawerTitle="Dashboard Configuration"
|
||||
isOpen={isSettingsDrawerOpen}
|
||||
onClose={(): void => setIsSettingsDrawerOpen(false)}
|
||||
>
|
||||
<DashboardSettings dashboard={dashboard} />
|
||||
</SettingsDrawer>
|
||||
</>
|
||||
)}
|
||||
{!isDashboardLocked && (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={onAddPanel}
|
||||
prefix={<Plus size="md" />}
|
||||
testId="add-panel-header"
|
||||
prefix={<Configure size="md" />}
|
||||
testId="show-drawer"
|
||||
onClick={(): void => setIsSettingsDrawerOpen(true)}
|
||||
size="md"
|
||||
>
|
||||
New Panel
|
||||
Configure
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<SettingsDrawer
|
||||
drawerTitle="Dashboard Configuration"
|
||||
isOpen={isSettingsDrawerOpen}
|
||||
onClose={(): void => setIsSettingsDrawerOpen(false)}
|
||||
>
|
||||
<DashboardSettings dashboard={dashboard} />
|
||||
</SettingsDrawer>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
prefix={<Braces size="md" />}
|
||||
testId="edit-json"
|
||||
onClick={(): void => setIsJsonEditorOpen(true)}
|
||||
size="md"
|
||||
>
|
||||
Edit as JSON
|
||||
</Button>
|
||||
{!isDashboardLocked && (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={onAddPanel}
|
||||
prefix={<Plus size="md" />}
|
||||
testId="add-panel-header"
|
||||
size="md"
|
||||
>
|
||||
New Panel
|
||||
</Button>
|
||||
)}
|
||||
<JsonEditorDrawer
|
||||
dashboard={dashboard}
|
||||
isOpen={isJsonEditorOpen}
|
||||
onClose={(): void => setIsJsonEditorOpen(false)}
|
||||
/>
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteOpen}
|
||||
title={`Delete dashboard"?`}
|
||||
@@ -223,6 +314,15 @@ function DashboardActions({
|
||||
onConfirm={handleConfirmDelete}
|
||||
onClose={(): void => setIsDeleteOpen(false)}
|
||||
/>
|
||||
<SectionTitleModal
|
||||
open={isNewSectionOpen}
|
||||
heading="New section"
|
||||
okText="Create section"
|
||||
initialValue=""
|
||||
isSaving={isAddingSection}
|
||||
onClose={(): void => setIsNewSectionOpen(false)}
|
||||
onSubmit={handleCreateSection}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
.dashboardInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 40%;
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboardTitleContainer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboardImage {
|
||||
@@ -21,9 +11,8 @@
|
||||
}
|
||||
|
||||
.dashboardTitle {
|
||||
flex: 1;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
max-width: fit-content;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
@@ -37,6 +26,19 @@
|
||||
cursor: text !important;
|
||||
}
|
||||
|
||||
.descriptionIcon {
|
||||
flex-shrink: 0;
|
||||
color: var(--l2-foreground);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.divider {
|
||||
flex-shrink: 0;
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: var(--l2-border);
|
||||
}
|
||||
|
||||
.dashboardTitleEditor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -54,8 +56,13 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Flexes into the remaining space and clips so the ResizeObserver can measure
|
||||
how many tags fit before collapsing the rest into a `+N` badge. */
|
||||
.dashboardTags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
flex: 1 1 0;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { KeyboardEvent } from 'react';
|
||||
import { Check, Globe, LockKeyhole, X } from '@signozhq/icons';
|
||||
import { Check, Globe, LockKeyhole, SolidInfoCircle, X } from '@signozhq/icons';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
@@ -9,6 +9,7 @@ import cx from 'classnames';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
|
||||
import styles from './DashboardInfo.module.scss';
|
||||
import { useVisibleTagCount } from './useVisibleTagCount';
|
||||
import { useDashboardStore } from '../../store/useDashboardStore';
|
||||
|
||||
interface DashboardInfoProps {
|
||||
@@ -45,6 +46,11 @@ function DashboardInfo({
|
||||
const hasTags = tags.length > 0;
|
||||
const hasDescription = !isEmpty(description);
|
||||
|
||||
const { containerRef, visibleCount } = useVisibleTagCount(tags);
|
||||
const needsOverflow = tags.length > visibleCount;
|
||||
const visibleTags = needsOverflow ? tags.slice(0, visibleCount) : tags;
|
||||
const remainingTags = needsOverflow ? tags.slice(visibleCount) : [];
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
@@ -56,83 +62,106 @@ function DashboardInfo({
|
||||
|
||||
return (
|
||||
<div className={styles.dashboardInfo}>
|
||||
<div className={styles.dashboardTitleContainer}>
|
||||
<img src={image} alt={title} className={styles.dashboardImage} />
|
||||
{isEditing ? (
|
||||
<div className={styles.dashboardTitleEditor}>
|
||||
<Input
|
||||
autoFocus
|
||||
value={draft}
|
||||
testId="dashboard-title-input"
|
||||
maxLength={120}
|
||||
className={styles.dashboardTitleInput}
|
||||
onChange={(e): void => onDraftChange(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="icon"
|
||||
className={styles.dashboardTitleActionButton}
|
||||
aria-label="Save title"
|
||||
testId="dashboard-title-save"
|
||||
onClick={onCommit}
|
||||
>
|
||||
<Check size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={styles.dashboardTitleActionButton}
|
||||
aria-label="Cancel title edit"
|
||||
testId="dashboard-title-cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<TooltipSimple title={title}>
|
||||
<Typography.Text
|
||||
className={cx(styles.dashboardTitle, {
|
||||
[styles.dashboardTitleHover]: canEdit,
|
||||
})}
|
||||
data-testid="dashboard-title"
|
||||
onClick={canEdit ? onStartEdit : undefined}
|
||||
>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
<img src={image} alt={title} className={styles.dashboardImage} />
|
||||
|
||||
{isPublicDashboard && (
|
||||
<TooltipSimple title="This dashboard is publicly accessible">
|
||||
<Globe size={14} />
|
||||
</TooltipSimple>
|
||||
)}
|
||||
|
||||
{isDashboardLocked && (
|
||||
<TooltipSimple title="This dashboard is locked">
|
||||
<LockKeyhole size={14} />
|
||||
</TooltipSimple>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasTags && (
|
||||
<div className={styles.dashboardTags}>
|
||||
{tags.map((tag) => (
|
||||
<Badge key={tag} color="warning" variant="outline">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
{isEditing ? (
|
||||
<div className={styles.dashboardTitleEditor}>
|
||||
<Input
|
||||
autoFocus
|
||||
value={draft}
|
||||
testId="dashboard-title-input"
|
||||
maxLength={120}
|
||||
className={styles.dashboardTitleInput}
|
||||
onChange={(e): void => onDraftChange(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="icon"
|
||||
className={styles.dashboardTitleActionButton}
|
||||
aria-label="Save title"
|
||||
testId="dashboard-title-save"
|
||||
onClick={onCommit}
|
||||
>
|
||||
<Check size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={styles.dashboardTitleActionButton}
|
||||
aria-label="Cancel title edit"
|
||||
testId="dashboard-title-cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<TooltipSimple title={title}>
|
||||
<Typography.Text
|
||||
className={cx(styles.dashboardTitle, {
|
||||
[styles.dashboardTitleHover]: canEdit,
|
||||
})}
|
||||
data-testid="dashboard-title"
|
||||
onClick={canEdit ? onStartEdit : undefined}
|
||||
>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
|
||||
{hasDescription && (
|
||||
<Typography.Text color="muted">{description}</Typography.Text>
|
||||
<TooltipSimple title={description}>
|
||||
<SolidInfoCircle
|
||||
className={styles.descriptionIcon}
|
||||
size={14}
|
||||
data-testid="dashboard-description-info"
|
||||
/>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
|
||||
{isPublicDashboard && (
|
||||
<TooltipSimple title="This dashboard is publicly accessible">
|
||||
<Globe size={14} />
|
||||
</TooltipSimple>
|
||||
)}
|
||||
|
||||
{isDashboardLocked && (
|
||||
<TooltipSimple title="This dashboard is locked">
|
||||
<LockKeyhole size={14} />
|
||||
</TooltipSimple>
|
||||
)}
|
||||
|
||||
{hasTags && (
|
||||
<>
|
||||
<span className={styles.divider} />
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={styles.dashboardTags}
|
||||
data-testid="dashboard-tags"
|
||||
>
|
||||
{visibleTags.map((tag) => (
|
||||
<Badge key={tag} color="warning" variant="outline">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
{remainingTags.length > 0 && (
|
||||
<TooltipSimple title={remainingTags.join(', ')}>
|
||||
<Badge
|
||||
color="warning"
|
||||
variant="outline"
|
||||
data-testid="dashboard-tags-overflow"
|
||||
>
|
||||
+{remainingTags.length}
|
||||
</Badge>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
BADGE_GAP,
|
||||
estimateBadgeWidth,
|
||||
OVERFLOW_BADGE_WIDTH,
|
||||
} from 'components/Alerts/LabelColumn/utils';
|
||||
|
||||
interface Result {
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
visibleCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures how many tags fit in the container and returns the visible count,
|
||||
* reserving room for the `+N` overflow badge. Reuses the badge-width estimation
|
||||
* from the alerts LabelColumn so dashboards and alerts overflow identically.
|
||||
*/
|
||||
export function useVisibleTagCount(tags: string[]): Result {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [visibleCount, setVisibleCount] = useState(tags.length);
|
||||
|
||||
const calculateVisible = useCallback(
|
||||
(width: number): number => {
|
||||
if (width <= 0) {
|
||||
return 1;
|
||||
}
|
||||
const availableWidth = width - OVERFLOW_BADGE_WIDTH - BADGE_GAP;
|
||||
let usedWidth = 0;
|
||||
let count = 0;
|
||||
for (const tag of tags) {
|
||||
const badgeWidth = estimateBadgeWidth(tag) + BADGE_GAP;
|
||||
if (usedWidth + badgeWidth > availableWidth && count > 0) {
|
||||
break;
|
||||
}
|
||||
usedWidth += badgeWidth;
|
||||
count += 1;
|
||||
}
|
||||
return Math.max(1, count);
|
||||
},
|
||||
[tags],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) {
|
||||
return undefined;
|
||||
}
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (entry && entry.contentRect.width > 0) {
|
||||
setVisibleCount(calculateVisible(entry.contentRect.width));
|
||||
}
|
||||
});
|
||||
observer.observe(container);
|
||||
if (container.clientWidth > 0) {
|
||||
setVisibleCount(calculateVisible(container.clientWidth));
|
||||
}
|
||||
return (): void => observer.disconnect();
|
||||
}, [calculateVisible]);
|
||||
|
||||
return { containerRef, visibleCount };
|
||||
}
|
||||
@@ -5,7 +5,9 @@
|
||||
color: var(--l2-foreground);
|
||||
background-color: var(--l1-background);
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 2px 0px var(--l2-border);
|
||||
box-shadow:
|
||||
0 1px 0 0 var(--l2-border),
|
||||
0 6px 12px -10px var(--l2-border);
|
||||
}
|
||||
|
||||
.dashboardPageToolbarSubContainer {
|
||||
@@ -16,5 +18,22 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toolbarRow2 {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
clear: both;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.timeCluster {
|
||||
float: right;
|
||||
margin: 0 0 0 16px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
.root {
|
||||
:global(.ant-drawer-wrapper-body) {
|
||||
border-left: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
:global(.ant-drawer-header) {
|
||||
height: 48px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
:global(.ant-drawer-title) {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-drawer-body) {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:global(.ant-drawer-footer) {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.editor {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.validation {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.validationValid {
|
||||
color: var(--bg-forest-400);
|
||||
}
|
||||
|
||||
.validationInvalid {
|
||||
color: var(--bg-cherry-400);
|
||||
}
|
||||
|
||||
.footerActions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { KeyboardEvent, useCallback } from 'react';
|
||||
import MEditor from '@monaco-editor/react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { Drawer } from 'antd';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
|
||||
import { defineJsonEditorTheme, JSON_EDITOR_THEME } from './editorTheme';
|
||||
import styles from './JsonEditorDrawer.module.scss';
|
||||
import JsonEditorToolbar from './JsonEditorToolbar';
|
||||
import { useJsonEditor } from './useJsonEditor';
|
||||
|
||||
interface JsonEditorDrawerProps {
|
||||
dashboard: DashboardtypesGettableDashboardV2DTO;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function JsonEditorDrawer({
|
||||
dashboard,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: JsonEditorDrawerProps): JSX.Element {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const { draft, setDraft, validity, isDirty, isSaving, format, reset, apply } =
|
||||
useJsonEditor({ dashboard, isOpen, onApplied: onClose });
|
||||
|
||||
const onCopy = useCallback((): void => {
|
||||
copyToClipboard(draft);
|
||||
toast.success('JSON copied to clipboard');
|
||||
}, [copyToClipboard, draft]);
|
||||
|
||||
const onDownload = useCallback((): void => {
|
||||
const blob = new Blob([draft], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${dashboard.name || 'dashboard'}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}, [draft, dashboard.name]);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLDivElement>): void => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
void apply();
|
||||
}
|
||||
},
|
||||
[apply],
|
||||
);
|
||||
|
||||
const applyDisabled = !isDirty || !validity.valid || isSaving;
|
||||
const validationText = validity.valid
|
||||
? `Valid JSON · ${validity.lineCount} lines`
|
||||
: `Line ${validity.errorLine ?? '?'} · ${validity.message ?? 'Invalid JSON'}`;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title="Dashboard JSON"
|
||||
placement="right"
|
||||
width={660}
|
||||
onClose={onClose}
|
||||
open={isOpen}
|
||||
rootClassName={styles.root}
|
||||
footer={
|
||||
<div className={styles.footer}>
|
||||
<Typography.Text
|
||||
className={cx(styles.validation, {
|
||||
[styles.validationValid]: validity.valid,
|
||||
[styles.validationInvalid]: !validity.valid,
|
||||
})}
|
||||
data-testid="json-editor-validation"
|
||||
>
|
||||
{validationText}
|
||||
</Typography.Text>
|
||||
<div className={styles.footerActions}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="md"
|
||||
testId="json-editor-cancel"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="md"
|
||||
testId="json-editor-apply"
|
||||
disabled={applyDisabled}
|
||||
onClick={(): void => void apply()}
|
||||
>
|
||||
Apply changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div className={styles.body} onKeyDown={onKeyDown}>
|
||||
<JsonEditorToolbar
|
||||
isDirty={isDirty}
|
||||
onFormat={format}
|
||||
onCopy={onCopy}
|
||||
onDownload={onDownload}
|
||||
onReset={reset}
|
||||
/>
|
||||
<div className={styles.editor}>
|
||||
<MEditor
|
||||
language="json"
|
||||
height="100%"
|
||||
value={draft}
|
||||
onChange={(value): void => setDraft(value ?? '')}
|
||||
options={{
|
||||
scrollbar: { alwaysConsumeMouseWheel: false },
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
fontFamily: 'Space Mono',
|
||||
}}
|
||||
theme="vs-dark"
|
||||
onMount={(editor, monaco): void => {
|
||||
defineJsonEditorTheme(monaco, editor.getContainerDomNode());
|
||||
monaco.editor.setTheme(JSON_EDITOR_THEME);
|
||||
void document.fonts.ready.then(() => monaco.editor.remeasureFonts());
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default JsonEditorDrawer;
|
||||
@@ -0,0 +1,12 @@
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { AlignLeft, Copy, Download, RotateCcw } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
|
||||
import styles from './JsonEditorToolbar.module.scss';
|
||||
|
||||
interface JsonEditorToolbarProps {
|
||||
isDirty: boolean;
|
||||
onFormat: () => void;
|
||||
onCopy: () => void;
|
||||
onDownload: () => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
function JsonEditorToolbar({
|
||||
isDirty,
|
||||
onFormat,
|
||||
onCopy,
|
||||
onDownload,
|
||||
onReset,
|
||||
}: JsonEditorToolbarProps): JSX.Element {
|
||||
return (
|
||||
<div className={styles.toolbar}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
prefix={<AlignLeft size={14} />}
|
||||
testId="json-editor-format"
|
||||
onClick={onFormat}
|
||||
>
|
||||
Format
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
prefix={<Copy size={14} />}
|
||||
testId="json-editor-copy"
|
||||
onClick={onCopy}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
prefix={<Download size={14} />}
|
||||
testId="json-editor-download"
|
||||
onClick={onDownload}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
<div className={styles.spacer} />
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
prefix={<RotateCcw size={14} />}
|
||||
testId="json-editor-reset"
|
||||
disabled={!isDirty}
|
||||
onClick={onReset}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default JsonEditorToolbar;
|
||||
@@ -0,0 +1,165 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import JsonEditorDrawer from '../JsonEditorDrawer';
|
||||
import { useJsonEditor } from '../useJsonEditor';
|
||||
|
||||
jest.mock('../useJsonEditor', () => ({ useJsonEditor: jest.fn() }));
|
||||
|
||||
jest.mock('@monaco-editor/react', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (next?: string) => void;
|
||||
}): JSX.Element => (
|
||||
<textarea
|
||||
aria-label="json-editor"
|
||||
data-testid="monaco"
|
||||
value={value}
|
||||
onChange={(e): void => onChange(e.target.value)}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
useCopyToClipboard: (): [unknown, jest.Mock] => [{}, jest.fn()],
|
||||
}));
|
||||
|
||||
const mockUseJsonEditor = useJsonEditor as jest.Mock;
|
||||
|
||||
const dashboard = {
|
||||
id: 'dash-1',
|
||||
name: 'My dashboard',
|
||||
} as unknown as DashboardtypesGettableDashboardV2DTO;
|
||||
|
||||
function hookValue(
|
||||
overrides: Partial<ReturnType<typeof useJsonEditor>> = {},
|
||||
): ReturnType<typeof useJsonEditor> {
|
||||
return {
|
||||
draft: '{\n "a": 1\n}',
|
||||
setDraft: jest.fn(),
|
||||
validity: { valid: true, lineCount: 3 },
|
||||
isDirty: true,
|
||||
isSaving: false,
|
||||
format: jest.fn(),
|
||||
reset: jest.fn(),
|
||||
apply: jest.fn().mockResolvedValue(undefined),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('JsonEditorDrawer', () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('renders the toolbar, editor and footer actions when open', () => {
|
||||
mockUseJsonEditor.mockReturnValue(hookValue());
|
||||
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
|
||||
|
||||
expect(screen.getByTestId('json-editor-format')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('json-editor-copy')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('json-editor-download')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('json-editor-reset')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('json-editor-apply')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('monaco')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows a valid status with the line count', () => {
|
||||
mockUseJsonEditor.mockReturnValue(
|
||||
hookValue({ validity: { valid: true, lineCount: 12 } }),
|
||||
);
|
||||
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
|
||||
|
||||
expect(screen.getByTestId('json-editor-validation')).toHaveTextContent(
|
||||
'Valid JSON · 12 lines',
|
||||
);
|
||||
});
|
||||
|
||||
it('shows the error line and message when invalid', () => {
|
||||
mockUseJsonEditor.mockReturnValue(
|
||||
hookValue({
|
||||
validity: {
|
||||
valid: false,
|
||||
lineCount: 4,
|
||||
errorLine: 3,
|
||||
message: 'Unexpected token',
|
||||
},
|
||||
}),
|
||||
);
|
||||
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
|
||||
|
||||
expect(screen.getByTestId('json-editor-validation')).toHaveTextContent(
|
||||
'Line 3 · Unexpected token',
|
||||
);
|
||||
});
|
||||
|
||||
it('disables Apply when not dirty, invalid, or saving', () => {
|
||||
mockUseJsonEditor.mockReturnValue(hookValue({ isDirty: false }));
|
||||
const { rerender } = render(
|
||||
<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />,
|
||||
);
|
||||
expect(screen.getByTestId('json-editor-apply')).toBeDisabled();
|
||||
|
||||
mockUseJsonEditor.mockReturnValue(
|
||||
hookValue({ validity: { valid: false, lineCount: 1 } }),
|
||||
);
|
||||
rerender(
|
||||
<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />,
|
||||
);
|
||||
expect(screen.getByTestId('json-editor-apply')).toBeDisabled();
|
||||
|
||||
mockUseJsonEditor.mockReturnValue(hookValue({ isSaving: true }));
|
||||
rerender(
|
||||
<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />,
|
||||
);
|
||||
expect(screen.getByTestId('json-editor-apply')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('wires toolbar and footer buttons to the hook callbacks', () => {
|
||||
const value = hookValue();
|
||||
mockUseJsonEditor.mockReturnValue(value);
|
||||
const onClose = jest.fn();
|
||||
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={onClose} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('json-editor-format'));
|
||||
expect(value.format).toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByTestId('json-editor-reset'));
|
||||
expect(value.reset).toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByTestId('json-editor-apply'));
|
||||
expect(value.apply).toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByTestId('json-editor-cancel'));
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('forwards editor changes to setDraft', () => {
|
||||
const value = hookValue();
|
||||
mockUseJsonEditor.mockReturnValue(value);
|
||||
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
|
||||
|
||||
fireEvent.change(screen.getByTestId('monaco'), {
|
||||
target: { value: '{"b":2}' },
|
||||
});
|
||||
expect(value.setDraft).toHaveBeenCalledWith('{"b":2}');
|
||||
});
|
||||
|
||||
it('applies on Cmd/Ctrl+Enter', () => {
|
||||
const value = hookValue();
|
||||
mockUseJsonEditor.mockReturnValue(value);
|
||||
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
|
||||
|
||||
fireEvent.keyDown(screen.getByTestId('monaco'), {
|
||||
key: 'Enter',
|
||||
metaKey: true,
|
||||
});
|
||||
expect(value.apply).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,179 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { updateDashboardV2 } from 'api/generated/services/dashboard';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
|
||||
import { useJsonEditor } from '../useJsonEditor';
|
||||
|
||||
const mockRefetch = jest.fn();
|
||||
const mockShowErrorModal = jest.fn();
|
||||
|
||||
jest.mock('../../../store/useDashboardStore', () => ({
|
||||
useDashboardStore: (selector: (state: unknown) => unknown): unknown =>
|
||||
selector({ dashboardId: 'dash-1', refetch: mockRefetch }),
|
||||
}));
|
||||
|
||||
jest.mock('providers/ErrorModalProvider', () => ({
|
||||
useErrorModal: (): { showErrorModal: jest.Mock } => ({
|
||||
showErrorModal: mockShowErrorModal,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('api/generated/services/dashboard', () => ({
|
||||
updateDashboardV2: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
const mockUpdate = updateDashboardV2 as jest.Mock;
|
||||
const mockToastSuccess = toast.success as jest.Mock;
|
||||
|
||||
const dashboard = {
|
||||
id: 'dash-1',
|
||||
name: 'My dashboard',
|
||||
schemaVersion: 'v6',
|
||||
image: 'icon.png',
|
||||
tags: [{ key: 'env', value: 'prod' }],
|
||||
spec: {
|
||||
display: { name: 'My dashboard' },
|
||||
panels: {},
|
||||
layouts: [],
|
||||
variables: [],
|
||||
},
|
||||
} as unknown as DashboardtypesGettableDashboardV2DTO;
|
||||
|
||||
const serialized = JSON.stringify(dashboard, null, 2);
|
||||
|
||||
describe('useJsonEditor', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUpdate.mockResolvedValue({});
|
||||
});
|
||||
|
||||
it('seeds the draft from the dashboard and reports valid, non-dirty state', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
|
||||
);
|
||||
|
||||
expect(result.current.draft).toBe(serialized);
|
||||
expect(result.current.isDirty).toBe(false);
|
||||
expect(result.current.validity.valid).toBe(true);
|
||||
expect(result.current.validity.lineCount).toBe(serialized.split('\n').length);
|
||||
});
|
||||
|
||||
it('flags invalid JSON with a line number and marks the draft dirty', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
|
||||
);
|
||||
|
||||
act(() => result.current.setDraft('{\n "name": ,\n}'));
|
||||
|
||||
expect(result.current.validity.valid).toBe(false);
|
||||
expect(result.current.validity.message).toBeDefined();
|
||||
expect(result.current.isDirty).toBe(true);
|
||||
});
|
||||
|
||||
it('format() pretty-prints valid JSON and leaves invalid JSON untouched', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
|
||||
);
|
||||
|
||||
act(() => result.current.setDraft('{"a":1}'));
|
||||
act(() => result.current.format());
|
||||
expect(result.current.draft).toBe('{\n "a": 1\n}');
|
||||
|
||||
act(() => result.current.setDraft('{bad'));
|
||||
act(() => result.current.format());
|
||||
expect(result.current.draft).toBe('{bad');
|
||||
});
|
||||
|
||||
it('reset() restores the last-applied text', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
|
||||
);
|
||||
|
||||
act(() => result.current.setDraft('edited'));
|
||||
expect(result.current.isDirty).toBe(true);
|
||||
|
||||
act(() => result.current.reset());
|
||||
expect(result.current.draft).toBe(serialized);
|
||||
expect(result.current.isDirty).toBe(false);
|
||||
});
|
||||
|
||||
it('apply() is a no-op when the draft is unchanged or invalid', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.apply();
|
||||
});
|
||||
expect(mockUpdate).not.toHaveBeenCalled();
|
||||
|
||||
act(() => result.current.setDraft('{bad'));
|
||||
await act(async () => {
|
||||
await result.current.apply();
|
||||
});
|
||||
expect(mockUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('apply() PUTs the narrowed body, toasts, refetches and calls onApplied', async () => {
|
||||
const onApplied = jest.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useJsonEditor({ dashboard, isOpen: true, onApplied }),
|
||||
);
|
||||
|
||||
const next = { ...dashboard, name: 'Renamed' };
|
||||
act(() => result.current.setDraft(JSON.stringify(next)));
|
||||
await act(async () => {
|
||||
await result.current.apply();
|
||||
});
|
||||
|
||||
expect(mockUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockUpdate).toHaveBeenCalledWith(
|
||||
{ id: 'dash-1' },
|
||||
expect.objectContaining({
|
||||
name: 'Renamed',
|
||||
schemaVersion: 'v6',
|
||||
spec: next.spec,
|
||||
tags: next.tags,
|
||||
}),
|
||||
);
|
||||
expect(mockToastSuccess).toHaveBeenCalled();
|
||||
expect(mockRefetch).toHaveBeenCalled();
|
||||
expect(onApplied).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('apply() surfaces errors through the error modal', async () => {
|
||||
mockUpdate.mockRejectedValueOnce(new Error('boom'));
|
||||
const { result } = renderHook(() =>
|
||||
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
|
||||
);
|
||||
|
||||
act(() =>
|
||||
result.current.setDraft(JSON.stringify({ ...dashboard, name: 'X' })),
|
||||
);
|
||||
await act(async () => {
|
||||
await result.current.apply();
|
||||
});
|
||||
|
||||
expect(mockShowErrorModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('re-seeds the draft when the drawer re-opens', () => {
|
||||
const onApplied = jest.fn();
|
||||
const { result, rerender } = renderHook(
|
||||
(props: { isOpen: boolean }) =>
|
||||
useJsonEditor({ dashboard, isOpen: props.isOpen, onApplied }),
|
||||
{ initialProps: { isOpen: false } },
|
||||
);
|
||||
|
||||
act(() => result.current.setDraft('stale edit'));
|
||||
expect(result.current.draft).toBe('stale edit');
|
||||
|
||||
rerender({ isOpen: true });
|
||||
expect(result.current.draft).toBe(serialized);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
DashboardtypesGettableDashboardV2DTO,
|
||||
DashboardtypesDashboardSpecDTO,
|
||||
DashboardtypesUpdatableDashboardV2DTO,
|
||||
TagtypesPostableTagDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
/**
|
||||
* Narrow a parsed (full Gettable-shaped) dashboard JSON down to the PUT-updatable
|
||||
* body. The editor shows the whole dashboard for readability, but the update
|
||||
* endpoint only accepts `{ name, schemaVersion, image, tags, spec }` — the
|
||||
* server owns `id`, `locked`, timestamps, etc., so we drop them here.
|
||||
*/
|
||||
export function dashboardToUpdatable(
|
||||
parsed: Record<string, unknown>,
|
||||
): DashboardtypesUpdatableDashboardV2DTO {
|
||||
const dashboard = parsed as Partial<DashboardtypesGettableDashboardV2DTO>;
|
||||
|
||||
return {
|
||||
name: dashboard.name ?? '',
|
||||
schemaVersion: dashboard.schemaVersion ?? 'v6',
|
||||
image: dashboard.image,
|
||||
tags: (dashboard.tags as TagtypesPostableTagDTO[] | null | undefined) ?? null,
|
||||
spec: dashboard.spec as DashboardtypesDashboardSpecDTO,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Monaco } from '@monaco-editor/react';
|
||||
|
||||
export const JSON_EDITOR_THEME = 'signoz-json';
|
||||
|
||||
function token(el: HTMLElement, name: string): string {
|
||||
return getComputedStyle(el).getPropertyValue(name).trim().replace('#', '');
|
||||
}
|
||||
|
||||
function isDark(hex: string): boolean {
|
||||
if (hex.length < 6) {
|
||||
return true;
|
||||
}
|
||||
const r = parseInt(hex.slice(0, 2), 16);
|
||||
const g = parseInt(hex.slice(2, 4), 16);
|
||||
const b = parseInt(hex.slice(4, 6), 16);
|
||||
return 0.299 * r + 0.587 * g + 0.114 * b < 128;
|
||||
}
|
||||
|
||||
export function defineJsonEditorTheme(monaco: Monaco, el: HTMLElement): void {
|
||||
const background = token(el, '--l1-background');
|
||||
const foreground = token(el, '--l1-foreground');
|
||||
const keyColor = token(el, '--bg-vanilla-400');
|
||||
const valueColor = token(el, '--bg-robin-400');
|
||||
|
||||
const rules: { token: string; foreground: string }[] = [];
|
||||
if (keyColor) {
|
||||
rules.push({ token: 'string.key.json', foreground: keyColor });
|
||||
}
|
||||
if (valueColor) {
|
||||
rules.push({ token: 'string.value.json', foreground: valueColor });
|
||||
}
|
||||
|
||||
const colors: Record<string, string> = {};
|
||||
if (background) {
|
||||
colors['editor.background'] = `#${background}`;
|
||||
}
|
||||
if (foreground) {
|
||||
colors['editor.foreground'] = `#${foreground}`;
|
||||
}
|
||||
|
||||
monaco.editor.defineTheme(JSON_EDITOR_THEME, {
|
||||
base: isDark(background) ? 'vs-dark' : 'vs',
|
||||
inherit: true,
|
||||
rules,
|
||||
colors,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { updateDashboardV2 } from 'api/generated/services/dashboard';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { dashboardToUpdatable } from './dashboardToUpdatable';
|
||||
import { useDashboardStore } from '../../store/useDashboardStore';
|
||||
|
||||
export interface JsonValidity {
|
||||
valid: boolean;
|
||||
lineCount: number;
|
||||
/** 1-based line of the parse error, when known. */
|
||||
errorLine?: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface Params {
|
||||
dashboard: DashboardtypesGettableDashboardV2DTO;
|
||||
isOpen: boolean;
|
||||
onApplied: () => void;
|
||||
}
|
||||
|
||||
interface Result {
|
||||
draft: string;
|
||||
setDraft: (next: string) => void;
|
||||
validity: JsonValidity;
|
||||
isDirty: boolean;
|
||||
isSaving: boolean;
|
||||
format: () => void;
|
||||
reset: () => void;
|
||||
apply: () => Promise<void>;
|
||||
}
|
||||
|
||||
const serialize = (dashboard: DashboardtypesGettableDashboardV2DTO): string =>
|
||||
JSON.stringify(dashboard, null, 2);
|
||||
|
||||
/** Derive a 1-based line number from a `JSON.parse` "position N" error message. */
|
||||
function errorLineFromMessage(
|
||||
source: string,
|
||||
message: string,
|
||||
): number | undefined {
|
||||
const match = /position (\d+)/.exec(message);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const position = Number(match[1]);
|
||||
return source.slice(0, position).split('\n').length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor state for the dashboard JSON drawer: tracks the editable `draft`
|
||||
* against the last-applied text, exposes live validation, and applies changes
|
||||
* via the full-document update endpoint.
|
||||
*/
|
||||
export function useJsonEditor({
|
||||
dashboard,
|
||||
isOpen,
|
||||
onApplied,
|
||||
}: Params): Result {
|
||||
const dashboardId = useDashboardStore((s) => s.dashboardId);
|
||||
const refetch = useDashboardStore((s) => s.refetch);
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const [appliedText, setAppliedText] = useState<string>(() =>
|
||||
serialize(dashboard),
|
||||
);
|
||||
const [draft, setDraft] = useState<string>(appliedText);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Re-seed the editor from the current dashboard each time the drawer opens so
|
||||
// it always reflects the latest persisted state (e.g. after a refetch).
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const next = serialize(dashboard);
|
||||
setAppliedText(next);
|
||||
setDraft(next);
|
||||
}
|
||||
}, [isOpen, dashboard]);
|
||||
|
||||
const validity = useMemo<JsonValidity>(() => {
|
||||
const lineCount = draft.split('\n').length;
|
||||
try {
|
||||
JSON.parse(draft);
|
||||
return { valid: true, lineCount };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Invalid JSON';
|
||||
return {
|
||||
valid: false,
|
||||
lineCount,
|
||||
errorLine: errorLineFromMessage(draft, message),
|
||||
message,
|
||||
};
|
||||
}
|
||||
}, [draft]);
|
||||
|
||||
const isDirty = draft !== appliedText;
|
||||
|
||||
const format = useCallback((): void => {
|
||||
try {
|
||||
setDraft(JSON.stringify(JSON.parse(draft), null, 2));
|
||||
} catch {
|
||||
// Leave the draft untouched when it can't be parsed.
|
||||
}
|
||||
}, [draft]);
|
||||
|
||||
const reset = useCallback((): void => {
|
||||
setDraft(appliedText);
|
||||
}, [appliedText]);
|
||||
|
||||
const apply = useCallback(async (): Promise<void> => {
|
||||
if (!validity.valid || !isDirty) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsSaving(true);
|
||||
const parsed = JSON.parse(draft) as Record<string, unknown>;
|
||||
await updateDashboardV2({ id: dashboardId }, dashboardToUpdatable(parsed));
|
||||
toast.success('Dashboard updated');
|
||||
refetch();
|
||||
onApplied();
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [
|
||||
dashboardId,
|
||||
validity.valid,
|
||||
isDirty,
|
||||
draft,
|
||||
refetch,
|
||||
onApplied,
|
||||
showErrorModal,
|
||||
]);
|
||||
|
||||
return {
|
||||
draft,
|
||||
setDraft,
|
||||
validity,
|
||||
isDirty,
|
||||
isSaving,
|
||||
format,
|
||||
reset,
|
||||
apply,
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
DashboardtypesJSONPatchOperationDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
@@ -139,7 +140,15 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VariablesBar dashboard={dashboard} />
|
||||
{/* Row 2: the time selector floats top-right (declared first so the
|
||||
variables bar's content wraps around it); the variables bar
|
||||
collapses to one line and, when expanded, wraps full-width under it. */}
|
||||
<div className={styles.toolbarRow2}>
|
||||
<div className={styles.timeCluster}>
|
||||
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
|
||||
</div>
|
||||
<VariablesBar dashboard={dashboard} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useDashboardStore } from '../../../store/useDashboardStore';
|
||||
import { useDeleteSection } from '../hooks/useDeleteSection';
|
||||
import { useRenameSection } from '../hooks/useRenameSection';
|
||||
import { useToggleSectionCollapse } from '../hooks/useToggleSectionCollapse';
|
||||
import RenameSectionModal from '../RenameSectionModal';
|
||||
import SectionTitleModal from '../SectionTitleModal';
|
||||
import SectionGrid from '../SectionGrid/SectionGrid';
|
||||
import SectionHeader, {
|
||||
type SectionDragHandle,
|
||||
@@ -146,8 +146,10 @@ function Section({
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<RenameSectionModal
|
||||
<SectionTitleModal
|
||||
open={isRenaming}
|
||||
heading="Rename section"
|
||||
okText="Rename"
|
||||
initialValue={section.title}
|
||||
isSaving={isSaving}
|
||||
onClose={(): void => setIsRenaming(false)}
|
||||
|
||||
@@ -2,21 +2,30 @@ import { useEffect, useState } from 'react';
|
||||
import { Modal } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
|
||||
interface RenameSectionModalProps {
|
||||
interface SectionTitleModalProps {
|
||||
open: boolean;
|
||||
/** Modal heading, e.g. "Rename section" / "New section". */
|
||||
heading: string;
|
||||
/** Confirm button label, e.g. "Rename" / "Create section". */
|
||||
okText: string;
|
||||
initialValue: string;
|
||||
isSaving: boolean;
|
||||
placeholder?: string;
|
||||
onClose: () => void;
|
||||
onSubmit: (title: string) => void;
|
||||
}
|
||||
|
||||
function RenameSectionModal({
|
||||
/** Title-entry modal shared by section create and rename. */
|
||||
function SectionTitleModal({
|
||||
open,
|
||||
heading,
|
||||
okText,
|
||||
initialValue,
|
||||
isSaving,
|
||||
placeholder = 'Section name',
|
||||
onClose,
|
||||
onSubmit,
|
||||
}: RenameSectionModalProps): JSX.Element {
|
||||
}: SectionTitleModalProps): JSX.Element {
|
||||
const [value, setValue] = useState<string>(initialValue);
|
||||
|
||||
// Reseed the field each time the modal opens.
|
||||
@@ -36,19 +45,19 @@ function RenameSectionModal({
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title="Rename section"
|
||||
title={heading}
|
||||
onCancel={onClose}
|
||||
onOk={submit}
|
||||
okText="Rename"
|
||||
okText={okText}
|
||||
okButtonProps={{ disabled: isSaving || !value.trim() }}
|
||||
destroyOnClose
|
||||
>
|
||||
<Input
|
||||
testId="rename-section-input"
|
||||
testId="section-title-input"
|
||||
autoFocus
|
||||
value={value}
|
||||
maxLength={120}
|
||||
placeholder="Section name"
|
||||
placeholder={placeholder}
|
||||
onChange={(e): void => setValue(e.target.value)}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -61,4 +70,4 @@ function RenameSectionModal({
|
||||
);
|
||||
}
|
||||
|
||||
export default RenameSectionModal;
|
||||
export default SectionTitleModal;
|
||||
@@ -12,6 +12,27 @@ import {
|
||||
} from '../../../patchOps';
|
||||
import { useDashboardStore } from '../../../store/useDashboardStore';
|
||||
|
||||
const SECTION_SELECTOR = '[data-testid^="dashboard-section-"]';
|
||||
|
||||
/**
|
||||
* Waits (via rAF) for the refetch to render the appended section, then scrolls
|
||||
* it into view. Polls because `refetch` resolves before React commits the new
|
||||
* section to the DOM; bails after ~40 frames.
|
||||
*/
|
||||
function scrollToNewSection(prevCount: number, attempts = 40): void {
|
||||
const sections = document.querySelectorAll(SECTION_SELECTOR);
|
||||
if (sections.length > prevCount) {
|
||||
sections[sections.length - 1]?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (attempts > 0) {
|
||||
requestAnimationFrame(() => scrollToNewSection(prevCount, attempts - 1));
|
||||
}
|
||||
}
|
||||
|
||||
interface Params {
|
||||
layouts: DashboardtypesLayoutDTO[] | undefined | null;
|
||||
}
|
||||
@@ -42,10 +63,12 @@ export function useAddSection({ layouts }: Params): Result {
|
||||
!layouts || layouts.length === 0
|
||||
? reorderLayoutsOp([newGridLayout(trimmed)])
|
||||
: addSectionOp(trimmed);
|
||||
const prevSectionCount = document.querySelectorAll(SECTION_SELECTOR).length;
|
||||
try {
|
||||
setIsSaving(true);
|
||||
await patchDashboardV2({ id: dashboardId }, [op]);
|
||||
refetch();
|
||||
scrollToNewSection(prevSectionCount);
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
} finally {
|
||||
|
||||
@@ -101,7 +101,7 @@ function VariableSelector({
|
||||
${variable.name}
|
||||
{variable.description ? (
|
||||
<Tooltip title={variable.description}>
|
||||
<SolidInfoCircle className={styles.infoIcon} size="md" />
|
||||
<SolidInfoCircle className={styles.infoIcon} size={14} />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Typography.Text>
|
||||
|
||||
@@ -1,12 +1,55 @@
|
||||
/* Mirrors the V1 dashboard variable bar: each variable is a connected pill —
|
||||
a robin `$name` segment joined to a value segment. */
|
||||
/* Sits inside the already-padded sticky toolbar section, so it only needs a top
|
||||
gap from the tags — horizontal/bottom padding comes from the toolbar. */
|
||||
.bar {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.strip {
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
.stripExpanded {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
overflow: visible;
|
||||
clear: both;
|
||||
|
||||
.variableSlot,
|
||||
.moreButton {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: variablesExpandIn 200ms ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes variablesExpandIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.variableSlot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.variableSlotHidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.moreButton {
|
||||
display: inline-flex;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.variableItem {
|
||||
@@ -21,7 +64,7 @@
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 6px 6px 8px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--l3-border);
|
||||
border-radius: 2px 0 0 2px;
|
||||
background: var(--l3-background);
|
||||
color: var(--bg-robin-300);
|
||||
@@ -33,8 +76,10 @@
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
margin-left: 4px;
|
||||
display: inline-flex;
|
||||
margin-left: 2px;
|
||||
color: var(--l2-foreground);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.variableValue {
|
||||
@@ -42,7 +87,7 @@
|
||||
min-width: 120px;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--l3-border);
|
||||
border-left: none;
|
||||
border-radius: 0 2px 2px 0;
|
||||
background: var(--l2-background);
|
||||
@@ -55,8 +100,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Inner control fills the value segment; the segment provides the frame, so the
|
||||
control itself is borderless/transparent. */
|
||||
.control {
|
||||
width: 100%;
|
||||
min-width: 120px;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronLeft } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import cx from 'classnames';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { useInlineOverflowCount } from 'hooks/useInlineOverflowCount';
|
||||
|
||||
import { useVariableSelection } from './useVariableSelection';
|
||||
import VariableSelector from './VariableSelector';
|
||||
@@ -11,33 +16,76 @@ interface VariablesBarProps {
|
||||
/**
|
||||
* Runtime variable selector bar shown above the panels. Renders one control per
|
||||
* dashboard variable; selections live in the store + URL (never the spec).
|
||||
*
|
||||
* The pills sit on the line left of the floated time selector and collapse the
|
||||
* overflow behind a `+N` trigger. Expanding lets the bar wrap onto full-width
|
||||
* lines that flow underneath the time selector. Every selector stays mounted
|
||||
* either way so auto-selection and option fetching keep driving the panels.
|
||||
*/
|
||||
function VariablesBar({ dashboard }: VariablesBarProps): JSX.Element | null {
|
||||
const { variables, dependencyData, selection, setSelection } =
|
||||
useVariableSelection(dashboard);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { containerRef, visibleCount, overflowCount } = useInlineOverflowCount({
|
||||
itemCount: variables.length,
|
||||
gap: 8,
|
||||
reserveWidth: 48,
|
||||
enabled: !expanded,
|
||||
});
|
||||
|
||||
if (variables.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasOverflow = overflowCount > 0;
|
||||
|
||||
return (
|
||||
<div className={styles.bar} data-testid="dashboard-variables-bar">
|
||||
{variables.map((variable) => (
|
||||
<VariableSelector
|
||||
key={variable.name}
|
||||
variable={variable}
|
||||
variables={variables}
|
||||
parents={dependencyData.parentGraph[variable.name] ?? []}
|
||||
selections={selection}
|
||||
selection={
|
||||
selection[variable.name] ?? {
|
||||
value: variable.multiSelect ? [] : '',
|
||||
allSelected: false,
|
||||
}
|
||||
}
|
||||
onChange={(next): void => setSelection(variable.name, next)}
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cx(styles.strip, { [styles.stripExpanded]: expanded })}
|
||||
>
|
||||
{variables.map((variable, index) => (
|
||||
<div
|
||||
key={variable.name}
|
||||
data-overflow-item="true"
|
||||
className={cx(styles.variableSlot, {
|
||||
[styles.variableSlotHidden]:
|
||||
!expanded && hasOverflow && index >= visibleCount,
|
||||
})}
|
||||
>
|
||||
<VariableSelector
|
||||
variable={variable}
|
||||
variables={variables}
|
||||
parents={dependencyData.parentGraph[variable.name] ?? []}
|
||||
selections={selection}
|
||||
selection={
|
||||
selection[variable.name] ?? {
|
||||
value: variable.multiSelect ? [] : '',
|
||||
allSelected: false,
|
||||
}
|
||||
}
|
||||
onChange={(next): void => setSelection(variable.name, next)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{hasOverflow && (
|
||||
<span className={styles.moreButton}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="md"
|
||||
prefix={expanded ? <ChevronLeft size={14} /> : undefined}
|
||||
aria-expanded={expanded}
|
||||
testId="dashboard-variables-more"
|
||||
onClick={(): void => setExpanded((prev) => !prev)}
|
||||
>
|
||||
{expanded ? 'Less' : `+${overflowCount}`}
|
||||
</Button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ function ValueSelector({
|
||||
|
||||
return (
|
||||
<CustomSelect
|
||||
className={styles.select}
|
||||
className={styles.control}
|
||||
data-testid={testId}
|
||||
options={optionData}
|
||||
value={
|
||||
|
||||
133
frontend/src/schemas/generated/transactionGroups.schema.json
Normal file
133
frontend/src/schemas/generated/transactionGroups.schema.json
Normal file
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"title": "TransactionGroups",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AuthtypesTransactionGroup"
|
||||
},
|
||||
"definitions": {
|
||||
"AuthtypesRelation": {
|
||||
"additionalProperties": false,
|
||||
"enum": [
|
||||
"create",
|
||||
"read",
|
||||
"update",
|
||||
"delete",
|
||||
"list",
|
||||
"assignee",
|
||||
"attach",
|
||||
"detach"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AuthtypesTransactionGroup": {
|
||||
"required": [
|
||||
"relation",
|
||||
"objectGroup"
|
||||
],
|
||||
"properties": {
|
||||
"objectGroup": {
|
||||
"$ref": "#/definitions/CoretypesObjectGroup"
|
||||
},
|
||||
"relation": {
|
||||
"$ref": "#/definitions/AuthtypesRelation"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"CoretypesKind": {
|
||||
"additionalProperties": false,
|
||||
"enum": [
|
||||
"anonymous",
|
||||
"organization",
|
||||
"role",
|
||||
"serviceaccount",
|
||||
"user",
|
||||
"notification-channel",
|
||||
"route-policy",
|
||||
"apdex-setting",
|
||||
"auth-domain",
|
||||
"session",
|
||||
"cloud-integration",
|
||||
"cloud-integration-service",
|
||||
"integration",
|
||||
"dashboard",
|
||||
"public-dashboard",
|
||||
"ingestion-key",
|
||||
"ingestion-limit",
|
||||
"pipeline",
|
||||
"user-preference",
|
||||
"org-preference",
|
||||
"quick-filter",
|
||||
"ttl-setting",
|
||||
"rule",
|
||||
"planned-maintenance",
|
||||
"saved-view",
|
||||
"trace-funnel",
|
||||
"factor-password",
|
||||
"factor-api-key",
|
||||
"license",
|
||||
"subscription",
|
||||
"logs",
|
||||
"traces",
|
||||
"metrics",
|
||||
"audit-logs",
|
||||
"meter-metrics",
|
||||
"logs-field",
|
||||
"traces-field"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CoretypesObjectGroup": {
|
||||
"required": [
|
||||
"resource",
|
||||
"selectors"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"resource": {
|
||||
"$ref": "#/definitions/CoretypesResourceRef"
|
||||
},
|
||||
"selectors": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/CoretypesSelector"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"CoretypesResourceRef": {
|
||||
"required": [
|
||||
"type",
|
||||
"kind"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"kind": {
|
||||
"$ref": "#/definitions/CoretypesKind"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/CoretypesType"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"CoretypesSelector": {
|
||||
"additionalProperties": false,
|
||||
"type": "string"
|
||||
},
|
||||
"CoretypesType": {
|
||||
"additionalProperties": false,
|
||||
"enum": [
|
||||
"user",
|
||||
"serviceaccount",
|
||||
"anonymous",
|
||||
"role",
|
||||
"organization",
|
||||
"metaresource",
|
||||
"telemetryresource"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "WebSettings",
|
||||
"required": [
|
||||
"posthog",
|
||||
"appcues",
|
||||
@@ -794,6 +794,13 @@ notifications - 2050
|
||||
background: color-mix(in srgb, var(--l3-background) 20%, transparent);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// Monaco Editor style overrides
|
||||
.monaco-editor .find-widget.visible {
|
||||
top: 30px !important;
|
||||
right: 45px !important;
|
||||
}
|
||||
|
||||
body.ai-assistant-panel-open {
|
||||
.PylonChat-bubbleFrameContainer,
|
||||
.PylonChat-chatWindowFrameContainer {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM docs/config/web-settings.json */
|
||||
/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM frontend/src/schemas/generated/webSettings.schema.json */
|
||||
|
||||
export interface WebSettings {
|
||||
appcues: Appcues;
|
||||
|
||||
@@ -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 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.",
|
||||
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.",
|
||||
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 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.",
|
||||
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.",
|
||||
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 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.",
|
||||
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.",
|
||||
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 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.",
|
||||
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.",
|
||||
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 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.",
|
||||
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.",
|
||||
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 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.",
|
||||
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.",
|
||||
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 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.",
|
||||
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.",
|
||||
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 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.",
|
||||
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.",
|
||||
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 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.",
|
||||
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.",
|
||||
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 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.",
|
||||
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.",
|
||||
Request: new(inframonitoringtypes.PostableDaemonSets),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(inframonitoringtypes.DaemonSets),
|
||||
@@ -200,5 +200,23 @@ 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
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/{metric_name}/attributes", handler.New(
|
||||
if err := router.Handle("/api/v2/metrics/attributes", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricAttributes),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricAttributes",
|
||||
@@ -88,7 +88,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/{metric_name}/metadata", handler.New(
|
||||
if err := router.Handle("/api/v2/metrics/metadata", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricMetadata),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricMetadata",
|
||||
@@ -96,6 +96,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
Summary: "Get metric metadata",
|
||||
Description: "This endpoint returns metadata information like metric description, unit, type, temporality, monotonicity for a specified metric",
|
||||
Request: nil,
|
||||
RequestQuery: new(metricsexplorertypes.MetricNameQuery),
|
||||
RequestContentType: "",
|
||||
Response: new(metricsexplorertypes.MetricMetadata),
|
||||
ResponseContentType: "application/json",
|
||||
@@ -107,7 +108,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/{metric_name}/metadata", handler.New(
|
||||
if err := router.Handle("/api/v2/metrics/metadata", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.metricsExplorerHandler.UpdateMetricMetadata),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateMetricMetadata",
|
||||
@@ -126,7 +127,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/{metric_name}/highlights", handler.New(
|
||||
if err := router.Handle("/api/v2/metrics/highlights", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricHighlights),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricHighlights",
|
||||
@@ -134,6 +135,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
Summary: "Get metric highlights",
|
||||
Description: "This endpoint returns highlights like number of datapoints, totaltimeseries, active time series, last received time for a specified metric",
|
||||
Request: nil,
|
||||
RequestQuery: new(metricsexplorertypes.MetricNameQuery),
|
||||
RequestContentType: "",
|
||||
Response: new(metricsexplorertypes.MetricHighlightsResponse),
|
||||
ResponseContentType: "application/json",
|
||||
@@ -145,7 +147,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/{metric_name}/alerts", handler.New(
|
||||
if err := router.Handle("/api/v2/metrics/alerts", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricAlerts),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricAlerts",
|
||||
@@ -153,6 +155,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
Summary: "Get metric alerts",
|
||||
Description: "This endpoint returns associated alerts for a specified metric",
|
||||
Request: nil,
|
||||
RequestQuery: new(metricsexplorertypes.MetricNameQuery),
|
||||
RequestContentType: "",
|
||||
Response: new(metricsexplorertypes.MetricAlertsResponse),
|
||||
ResponseContentType: "application/json",
|
||||
@@ -164,7 +167,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/{metric_name}/dashboards", handler.New(
|
||||
if err := router.Handle("/api/v2/metrics/dashboards", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricDashboards),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricDashboards",
|
||||
@@ -172,6 +175,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
Summary: "Get metric dashboards",
|
||||
Description: "This endpoint returns associated dashboards for a specified metric",
|
||||
Request: nil,
|
||||
RequestQuery: new(metricsexplorertypes.MetricNameQuery),
|
||||
RequestContentType: "",
|
||||
Response: new(metricsexplorertypes.MetricDashboardsResponse),
|
||||
ResponseContentType: "application/json",
|
||||
|
||||
@@ -92,7 +92,7 @@ type AuthZ interface {
|
||||
}
|
||||
|
||||
// OnBeforeRoleDelete is a callback invoked before a role is deleted.
|
||||
type OnBeforeRoleDelete func(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, roleName string) error
|
||||
type OnBeforeRoleDelete func(context.Context, valuer.UUID, valuer.UUID) error
|
||||
|
||||
type Handler interface {
|
||||
Create(http.ResponseWriter, *http.Request)
|
||||
|
||||
@@ -44,7 +44,3 @@ type Handler interface {
|
||||
Update(http.ResponseWriter, *http.Request)
|
||||
Delete(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type Getter interface {
|
||||
OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, roleName string) error
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package implauthdomain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type getter struct {
|
||||
store authtypes.AuthDomainStore
|
||||
}
|
||||
|
||||
func NewGetter(store authtypes.AuthDomainStore) authdomain.Getter {
|
||||
return &getter{store: store}
|
||||
}
|
||||
|
||||
func (getter *getter) OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, roleName string) error {
|
||||
domains, err := getter.store.ListByOrgID(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
referencedBy := make([]string, 0)
|
||||
for _, domain := range domains {
|
||||
for _, mappedRole := range domain.AuthDomainConfig().RoleMapping.RoleNames() {
|
||||
if mappedRole == roleName {
|
||||
referencedBy = append(referencedBy, domain.StorableAuthDomain().Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(referencedBy) > 0 {
|
||||
return errors.WithAdditionalf(
|
||||
errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleHasAuthDomainMappings, "role is referenced by an SSO role mapping, remove it before deleting"),
|
||||
"referenced by auth domain(s): %s", strings.Join(referencedBy, ", "),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -13,18 +12,13 @@ import (
|
||||
type module struct {
|
||||
store authtypes.AuthDomainStore
|
||||
authNs map[authtypes.AuthNProvider]authn.AuthN
|
||||
authz authz.AuthZ
|
||||
}
|
||||
|
||||
func NewModule(store authtypes.AuthDomainStore, authNs map[authtypes.AuthNProvider]authn.AuthN, authz authz.AuthZ) authdomain.Module {
|
||||
return &module{store: store, authNs: authNs, authz: authz}
|
||||
func NewModule(store authtypes.AuthDomainStore, authNs map[authtypes.AuthNProvider]authn.AuthN) authdomain.Module {
|
||||
return &module{store: store, authNs: authNs}
|
||||
}
|
||||
|
||||
func (module *module) Create(ctx context.Context, domain *authtypes.AuthDomain) error {
|
||||
if err := module.validateRoleMapping(ctx, domain); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.Create(ctx, domain)
|
||||
}
|
||||
|
||||
@@ -56,10 +50,6 @@ func (module *module) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*au
|
||||
}
|
||||
|
||||
func (module *module) Update(ctx context.Context, domain *authtypes.AuthDomain) error {
|
||||
if err := module.validateRoleMapping(ctx, domain); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.Update(ctx, domain)
|
||||
}
|
||||
|
||||
@@ -84,13 +74,3 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (module *module) validateRoleMapping(ctx context.Context, domain *authtypes.AuthDomain) error {
|
||||
roleNames := domain.AuthDomainConfig().RoleMapping.RoleNames()
|
||||
if len(roleNames) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := module.authz.ListByOrgIDAndNames(ctx, domain.StorableAuthDomain().OrgID, roleNames)
|
||||
return err
|
||||
}
|
||||
|
||||
114
pkg/modules/inframonitoring/implinframonitoring/checks.go
Normal file
114
pkg/modules/inframonitoring/implinframonitoring/checks.go
Normal file
@@ -0,0 +1,114 @@
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
246
pkg/modules/inframonitoring/implinframonitoring/checks_test.go
Normal file
246
pkg/modules/inframonitoring/implinframonitoring/checks_test.go
Normal file
@@ -0,0 +1,246 @@
|
||||
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,6 +22,30 @@ 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,20 +413,42 @@ 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.
|
||||
//
|
||||
// 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) {
|
||||
// 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) {
|
||||
if len(metricNames) == 0 {
|
||||
return nil, 0, nil
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("metric_name", "count(*) AS cnt", "min(first_reported_unix_milli) AS min_first_reported")
|
||||
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.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.AttributesMetadataTableName))
|
||||
sb.Where(sb.In("metric_name", sqlbuilder.List(metricNames)))
|
||||
sb.GroupBy("metric_name")
|
||||
@@ -435,42 +457,73 @@ func (m *module) getMetricsExistenceAndEarliestTime(ctx context.Context, metricN
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type metricInfo struct {
|
||||
count uint64
|
||||
minFirstReported uint64
|
||||
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
|
||||
}
|
||||
}
|
||||
found := make(map[string]metricInfo, len(metricNames))
|
||||
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()
|
||||
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var cnt, minFR uint64
|
||||
if err := rows.Scan(&name, &cnt, &minFR); err != nil {
|
||||
return nil, 0, err
|
||||
var cnt uint64
|
||||
if err := rows.Scan(&name, &cnt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if name != "" && cnt > 0 {
|
||||
present[name] = true
|
||||
}
|
||||
found[name] = metricInfo{count: cnt, minFirstReported: minFR}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
return present, nil
|
||||
}
|
||||
|
||||
// getMetadata fetches the latest values of additionalCols for each unique combination of groupBy keys,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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.
|
||||
|
||||
@@ -29,3 +31,50 @@ 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,6 +49,84 @@ 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")
|
||||
|
||||
@@ -78,26 +156,18 @@ func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
|
||||
}
|
||||
|
||||
// 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 req.End is before the earliest reported time for these metrics, return early
|
||||
// with endTimeBeforeRetention=true.
|
||||
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(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.
|
||||
@@ -191,23 +261,16 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
|
||||
}
|
||||
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, podsTableMetricNamesList)
|
||||
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(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 {
|
||||
@@ -276,23 +339,16 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
|
||||
}
|
||||
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, nodesTableMetricNamesList)
|
||||
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(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 {
|
||||
@@ -366,23 +422,16 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
|
||||
}
|
||||
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, namespacesTableMetricNamesList)
|
||||
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(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 {
|
||||
@@ -450,23 +499,16 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
|
||||
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
|
||||
}
|
||||
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, clustersTableMetricNamesList)
|
||||
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(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 {
|
||||
@@ -547,23 +589,16 @@ func (m *module) ListVolumes(ctx context.Context, orgID valuer.UUID, req *infram
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(volumesBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, volumesTableMetricNamesList)
|
||||
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(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 {
|
||||
@@ -632,23 +667,16 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(deploymentsBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, deploymentsTableMetricNamesList)
|
||||
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(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 {
|
||||
@@ -722,23 +750,16 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(statefulSetsBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, statefulSetsTableMetricNamesList)
|
||||
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(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 {
|
||||
@@ -814,23 +835,16 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(jobsBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, jobsTableMetricNamesList)
|
||||
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(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 {
|
||||
@@ -906,23 +920,16 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(daemonSetsBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, daemonSetsTableMetricNamesList)
|
||||
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(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,6 +20,7 @@ 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 {
|
||||
@@ -34,4 +35,5 @@ 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,19 +6,22 @@ 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
|
||||
store llmpricingruletypes.Store
|
||||
flagger flagger.Flagger
|
||||
}
|
||||
|
||||
func NewModule(store llmpricingruletypes.Store) llmpricingrule.Module {
|
||||
return &module{store: store}
|
||||
func NewModule(store llmpricingruletypes.Store, flagger flagger.Flagger) llmpricingrule.Module {
|
||||
return &module{store: store, flagger: flagger}
|
||||
}
|
||||
|
||||
func (module *module) List(ctx context.Context, orgID valuer.UUID, offset, limit int, search string, isOverride *bool) ([]*llmpricingruletypes.LLMPricingRule, int, error) {
|
||||
@@ -89,6 +92,16 @@ 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
|
||||
|
||||
@@ -11,17 +11,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metricsexplorertypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func extractMetricName(req *http.Request) (string, error) {
|
||||
metricName := mux.Vars(req)["metric_name"]
|
||||
if metricName == "" {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "metric_name is required in URL path")
|
||||
}
|
||||
return metricName, nil
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
module metricsexplorer.Module
|
||||
}
|
||||
@@ -116,23 +107,17 @@ func (h *handler) UpdateMetricMetadata(rw http.ResponseWriter, req *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
// Extract metric_name from URL path
|
||||
vars := mux.Vars(req)
|
||||
metricName := vars["metric_name"]
|
||||
|
||||
if metricName == "" {
|
||||
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "metric_name is required in URL path"))
|
||||
return
|
||||
}
|
||||
|
||||
var in metricsexplorertypes.UpdateMetricMetadataRequest
|
||||
if err := binding.JSON.BindBody(req.Body, &in); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Set metric name from URL path
|
||||
in.MetricName = metricName
|
||||
if in.MetricName == "" {
|
||||
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName is required"))
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
err = h.module.UpdateMetricMetadata(req.Context(), orgID, &in)
|
||||
@@ -151,11 +136,16 @@ func (h *handler) GetMetricMetadata(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
metricName, err := extractMetricName(req)
|
||||
if err != nil {
|
||||
var in metricsexplorertypes.MetricNameQuery
|
||||
if err := binding.Query.BindQuery(req.URL.Query(), &in); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
if err := in.Validate(); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
metricName := in.MetricName
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
@@ -181,20 +171,24 @@ func (h *handler) GetMetricAlerts(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
metricName, err := extractMetricName(req)
|
||||
if err != nil {
|
||||
var in metricsexplorertypes.MetricNameQuery
|
||||
if err := binding.Query.BindQuery(req.URL.Query(), &in); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
if err := in.Validate(); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
if err := h.checkMetricExists(req.Context(), orgID, metricName); err != nil {
|
||||
if err := h.checkMetricExists(req.Context(), orgID, in.MetricName); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := h.module.GetMetricAlerts(req.Context(), orgID, metricName)
|
||||
out, err := h.module.GetMetricAlerts(req.Context(), orgID, in.MetricName)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
@@ -209,20 +203,24 @@ func (h *handler) GetMetricDashboards(rw http.ResponseWriter, req *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
metricName, err := extractMetricName(req)
|
||||
if err != nil {
|
||||
var in metricsexplorertypes.MetricNameQuery
|
||||
if err := binding.Query.BindQuery(req.URL.Query(), &in); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
if err := in.Validate(); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
if err := h.checkMetricExists(req.Context(), orgID, metricName); err != nil {
|
||||
if err := h.checkMetricExists(req.Context(), orgID, in.MetricName); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := h.module.GetMetricDashboards(req.Context(), orgID, metricName)
|
||||
out, err := h.module.GetMetricDashboards(req.Context(), orgID, in.MetricName)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
@@ -237,20 +235,24 @@ func (h *handler) GetMetricHighlights(rw http.ResponseWriter, req *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
metricName, err := extractMetricName(req)
|
||||
if err != nil {
|
||||
var in metricsexplorertypes.MetricNameQuery
|
||||
if err := binding.Query.BindQuery(req.URL.Query(), &in); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
if err := in.Validate(); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
if err := h.checkMetricExists(req.Context(), orgID, metricName); err != nil {
|
||||
if err := h.checkMetricExists(req.Context(), orgID, in.MetricName); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
highlights, err := h.module.GetMetricHighlights(req.Context(), orgID, metricName)
|
||||
highlights, err := h.module.GetMetricHighlights(req.Context(), orgID, in.MetricName)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
@@ -265,20 +267,12 @@ func (h *handler) GetMetricAttributes(rw http.ResponseWriter, req *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
metricName, err := extractMetricName(req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var in metricsexplorertypes.MetricAttributesRequest
|
||||
if err := binding.Query.BindQuery(req.URL.Query(), &in); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
in.MetricName = metricName
|
||||
|
||||
if err := in.Validate(); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
@@ -286,7 +280,7 @@ func (h *handler) GetMetricAttributes(rw http.ResponseWriter, req *http.Request)
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
if err := h.checkMetricExists(req.Context(), orgID, metricName); err != nil {
|
||||
if err := h.checkMetricExists(req.Context(), orgID, in.MetricName); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func NewGetter(store serviceaccounttypes.Store) serviceaccount.Getter {
|
||||
return &getter{store: store}
|
||||
}
|
||||
|
||||
func (getter *getter) OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, _ string) error {
|
||||
func (getter *getter) OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) error {
|
||||
serviceAccounts, err := getter.store.GetServiceAccountsByOrgIDAndRoleID(ctx, orgID, roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
type Getter interface {
|
||||
// OnBeforeRoleDelete checks if any service accounts are assigned to the role and rejects deletion if so.
|
||||
OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, roleName string) error
|
||||
OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) error
|
||||
}
|
||||
|
||||
type Module interface {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||
@@ -30,10 +29,9 @@ type module struct {
|
||||
authDomain authdomain.Module
|
||||
tokenizer tokenizer.Tokenizer
|
||||
orgGetter organization.Getter
|
||||
authz authz.AuthZ
|
||||
}
|
||||
|
||||
func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, userSetter user.Setter, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter, authz authz.AuthZ) session.Module {
|
||||
func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, userSetter user.Setter, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter) session.Module {
|
||||
return &module{
|
||||
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/session/implsession"),
|
||||
authNs: authNs,
|
||||
@@ -42,7 +40,6 @@ func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.A
|
||||
authDomain: authDomain,
|
||||
tokenizer: tokenizer,
|
||||
orgGetter: orgGetter,
|
||||
authz: authz,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,23 +143,15 @@ func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvi
|
||||
}
|
||||
|
||||
roleMapping := authDomain.AuthDomainConfig().RoleMapping
|
||||
|
||||
roleAttributeExists := false
|
||||
if roleMapping != nil && roleMapping.UseRoleAttribute && callbackIdentity.Role != "" {
|
||||
_, err := module.authz.GetByOrgIDAndName(ctx, callbackIdentity.OrgID, authtypes.NormalizeRoleName(callbackIdentity.Role))
|
||||
if err == nil {
|
||||
roleAttributeExists = true
|
||||
}
|
||||
}
|
||||
|
||||
roleNames := roleMapping.NewRolesFromCallbackIdentity(callbackIdentity, roleAttributeExists)
|
||||
role := roleMapping.NewRoleFromCallbackIdentity(callbackIdentity)
|
||||
signozManagedRole := authtypes.MustGetSigNozManagedRoleFromExistingRole(role)
|
||||
|
||||
newUser, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, callbackIdentity.OrgID, types.UserStatusActive)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
newUser, err = module.userSetter.GetOrCreateUser(ctx, newUser, user.WithRoleNames(roleNames))
|
||||
newUser, err = module.userSetter.GetOrCreateUser(ctx, newUser, user.WithRoleNames([]string{signozManagedRole}))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -4,19 +4,22 @@ 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
|
||||
store spantypes.SpanMapperStore
|
||||
flagger flagger.Flagger
|
||||
}
|
||||
|
||||
func NewModule(store spantypes.SpanMapperStore) spanmapper.Module {
|
||||
return &module{store: store}
|
||||
func NewModule(store spantypes.SpanMapperStore, flagger flagger.Flagger) spanmapper.Module {
|
||||
return &module{store: store, flagger: flagger}
|
||||
}
|
||||
|
||||
func (module *module) ListGroups(ctx context.Context, orgID valuer.UUID, q *spantypes.ListSpanMapperGroupsQuery) ([]*spantypes.SpanMapperGroup, error) {
|
||||
@@ -109,12 +112,22 @@ func (module *module) AgentFeatureType() agentConf.AgentFeatureType {
|
||||
func (module *module) RecommendAgentConfig(orgID valuer.UUID, currentConfYaml []byte, configVersion *opamptypes.AgentConfigVersion) ([]byte, string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
enabled, err := module.listEnabledGroupsWithMappers(ctx, orgID)
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
updatedConf, err := spantypes.GenerateCollectorConfigWithSpanMapperProcessor(currentConfYaml, enabled)
|
||||
updatedConf, err := spantypes.GenerateCollectorConfigWithSpanMapperProcessor(currentConfYaml, enabledMappers)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ func (module *getter) VerifyResetPasswordToken(ctx context.Context, token string
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *getter) OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, _ string) error {
|
||||
func (module *getter) OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) error {
|
||||
users, err := module.GetUsersByOrgIDAndRoleID(ctx, orgID, roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -96,7 +96,7 @@ type Getter interface {
|
||||
GetUsersByOrgIDAndRoleID(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) ([]*types.User, error)
|
||||
|
||||
// OnBeforeRoleDelete checks if any users are assigned to the role and rejects deletion if so.
|
||||
OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, roleName string) error
|
||||
OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) error
|
||||
|
||||
// VerifyResetPasswordToken checks if a reset password token exists and is not expired.
|
||||
VerifyResetPasswordToken(ctx context.Context, token string) error
|
||||
|
||||
@@ -113,6 +113,8 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
Store: signoz.SQLStore,
|
||||
AgentFeatures: []agentConf.AgentFeature{
|
||||
logParsingPipelineController,
|
||||
signoz.Modules.SpanMapper,
|
||||
signoz.Modules.LLMPricingRule,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -128,7 +128,6 @@ func NewModules(
|
||||
}
|
||||
userSetter := impluser.NewSetter(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, authz, analytics, config.User, userRoleStore, userGetter, onDeleteUser)
|
||||
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
||||
authDomainModule := implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs, authz)
|
||||
|
||||
return Modules{
|
||||
OrgGetter: orgGetter,
|
||||
@@ -143,8 +142,8 @@ func NewModules(
|
||||
QuickFilter: quickfilter,
|
||||
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
|
||||
RawDataExport: implrawdataexport.NewModule(querier),
|
||||
AuthDomain: authDomainModule,
|
||||
Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, authDomainModule, tokenizer, orgGetter, authz),
|
||||
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),
|
||||
@@ -155,8 +154,8 @@ func NewModules(
|
||||
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)),
|
||||
SpanMapper: implspanmapper.NewModule(implspanmapper.NewStore(sqlstore), fl),
|
||||
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), fl),
|
||||
Tag: tagModule,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewRecreateUserDashboardPreferenceFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewMigrateRecurrenceBoundsFactory(sqlstore),
|
||||
sqlmigration.NewAddDashboardViewFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewMigrateSSORoleMappingNamesFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/meterreporter"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain/implauthdomain"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
@@ -350,13 +349,10 @@ func New(
|
||||
// Initialize service account getter
|
||||
serviceAccountGetter := implserviceaccount.NewGetter(implserviceaccount.NewStore(sqlstore))
|
||||
|
||||
authDomainGetter := implauthdomain.NewGetter(implauthdomain.NewStore(sqlstore))
|
||||
|
||||
// Build pre-delete callbacks from modules
|
||||
onBeforeRoleDelete := []authz.OnBeforeRoleDelete{
|
||||
userGetter.OnBeforeRoleDelete,
|
||||
serviceAccountGetter.OnBeforeRoleDelete,
|
||||
authDomainGetter.OnBeforeRoleDelete,
|
||||
}
|
||||
|
||||
// Initialize authz
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type migrateSSORoleMappingNames struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
type authDomainRow struct {
|
||||
bun.BaseModel `bun:"table:auth_domain"`
|
||||
|
||||
ID string `bun:"id"`
|
||||
Data string `bun:"data"`
|
||||
}
|
||||
|
||||
var legacyRoleToManagedRoleName = map[string]string{
|
||||
"ADMIN": "signoz-admin",
|
||||
"EDITOR": "signoz-editor",
|
||||
"VIEWER": "signoz-viewer",
|
||||
}
|
||||
|
||||
type ssoRoleMapping struct {
|
||||
DefaultRole string `json:"defaultRole"`
|
||||
GroupMappings map[string]string `json:"groupMappings"`
|
||||
UseRoleAttribute bool `json:"useRoleAttribute"`
|
||||
}
|
||||
|
||||
func NewMigrateSSORoleMappingNamesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("migrate_sso_role_mapping_names"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &migrateSSORoleMappingNames{sqlstore: sqlstore, logger: ps.Logger}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (migration *migrateSSORoleMappingNames) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *migrateSSORoleMappingNames) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
rows := make([]*authDomainRow, 0)
|
||||
if err := tx.NewSelect().Model(&rows).Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
config := make(map[string]json.RawMessage)
|
||||
if err := json.Unmarshal([]byte(row.Data), &config); err != nil {
|
||||
migration.logger.WarnContext(ctx, "skipping auth domain with unreadable data", slog.String("auth_domain_id", row.ID), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
|
||||
roleMappingRaw, ok := config["roleMapping"]
|
||||
if !ok || string(roleMappingRaw) == "null" {
|
||||
continue
|
||||
}
|
||||
|
||||
var roleMapping ssoRoleMapping
|
||||
if err := json.Unmarshal(roleMappingRaw, &roleMapping); err != nil {
|
||||
migration.logger.WarnContext(ctx, "skipping auth domain with unreadable role mapping", slog.String("auth_domain_id", row.ID), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
|
||||
changed := false
|
||||
if managed, ok := legacyRoleToManagedRoleName[strings.ToUpper(roleMapping.DefaultRole)]; ok {
|
||||
roleMapping.DefaultRole = managed
|
||||
changed = true
|
||||
}
|
||||
for group, role := range roleMapping.GroupMappings {
|
||||
if managed, ok := legacyRoleToManagedRoleName[strings.ToUpper(role)]; ok {
|
||||
roleMapping.GroupMappings[group] = managed
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
newRoleMapping, err := json.Marshal(roleMapping)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config["roleMapping"] = newRoleMapping
|
||||
|
||||
newData, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.NewUpdate().
|
||||
Model((*authDomainRow)(nil)).
|
||||
Set("data = ?", string(newData)).
|
||||
Where("id = ?", row.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *migrateSSORoleMappingNames) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -2,6 +2,10 @@ package authtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
)
|
||||
|
||||
type AttributeMapping struct {
|
||||
@@ -47,95 +51,83 @@ func (attr *AttributeMapping) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
type RoleMapping struct {
|
||||
// Default role assigned to new SSO users when no group mapping applies.
|
||||
// Default role any new SSO users. Defaults to "VIEWER"
|
||||
DefaultRole string `json:"defaultRole"`
|
||||
|
||||
// Map of IDP group name to SigNoz role name.
|
||||
// Map of IDP group names to SigNoz roles. Key is group name, value is SigNoz role
|
||||
GroupMappings map[string]string `json:"groupMappings"`
|
||||
|
||||
// If true, use the role claim directly from IDP instead of group mappings.
|
||||
// If true, use the role claim directly from IDP instead of group mappings
|
||||
UseRoleAttribute bool `json:"useRoleAttribute"`
|
||||
}
|
||||
|
||||
func (roleMapping *RoleMapping) UnmarshalJSON(data []byte) error {
|
||||
type alias RoleMapping
|
||||
func (typ *RoleMapping) UnmarshalJSON(data []byte) error {
|
||||
type Alias RoleMapping
|
||||
|
||||
var temp alias
|
||||
var temp Alias
|
||||
if err := json.Unmarshal(data, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
temp.DefaultRole = NormalizeRoleName(temp.DefaultRole)
|
||||
for group, role := range temp.GroupMappings {
|
||||
temp.GroupMappings[group] = NormalizeRoleName(role)
|
||||
if temp.DefaultRole != "" {
|
||||
if _, err := types.NewRole(strings.ToUpper(temp.DefaultRole)); err != nil {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid default role %s", temp.DefaultRole)
|
||||
}
|
||||
}
|
||||
|
||||
*roleMapping = RoleMapping(temp)
|
||||
for group, role := range temp.GroupMappings {
|
||||
if _, err := types.NewRole(strings.ToUpper(role)); err != nil {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid role %s for group %s", role, group)
|
||||
}
|
||||
}
|
||||
|
||||
*typ = RoleMapping(temp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (roleMapping *RoleMapping) NewRolesFromCallbackIdentity(callbackIdentity *CallbackIdentity, roleAttributeExists bool) []string {
|
||||
func (roleMapping *RoleMapping) NewRoleFromCallbackIdentity(callbackIdentity *CallbackIdentity) types.Role {
|
||||
if roleMapping == nil {
|
||||
return []string{SigNozViewerRoleName}
|
||||
return types.RoleViewer
|
||||
}
|
||||
|
||||
if roleAttributeExists {
|
||||
return []string{NormalizeRoleName(callbackIdentity.Role)}
|
||||
if roleMapping.UseRoleAttribute && callbackIdentity.Role != "" {
|
||||
if role, err := types.NewRole(strings.ToUpper(callbackIdentity.Role)); err == nil {
|
||||
return role
|
||||
}
|
||||
}
|
||||
|
||||
if len(roleMapping.GroupMappings) > 0 && len(callbackIdentity.Groups) > 0 {
|
||||
roleNames := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
highestRole := types.RoleViewer
|
||||
found := false
|
||||
|
||||
for _, group := range callbackIdentity.Groups {
|
||||
roleName, exists := roleMapping.GroupMappings[group]
|
||||
if !exists {
|
||||
continue
|
||||
if mappedRole, exists := roleMapping.GroupMappings[group]; exists {
|
||||
found = true
|
||||
if role, err := types.NewRole(strings.ToUpper(mappedRole)); err == nil {
|
||||
if compareRoles(role, highestRole) > 0 {
|
||||
highestRole = role
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, duplicate := seen[roleName]; duplicate {
|
||||
continue
|
||||
}
|
||||
seen[roleName] = struct{}{}
|
||||
roleNames = append(roleNames, roleName)
|
||||
}
|
||||
if len(roleNames) > 0 {
|
||||
return roleNames
|
||||
|
||||
if found {
|
||||
return highestRole
|
||||
}
|
||||
}
|
||||
|
||||
return []string{roleMapping.DefaultRoleName()}
|
||||
}
|
||||
|
||||
func (roleMapping *RoleMapping) DefaultRoleName() string {
|
||||
if roleMapping.DefaultRole != "" {
|
||||
return roleMapping.DefaultRole
|
||||
}
|
||||
|
||||
return SigNozViewerRoleName
|
||||
}
|
||||
|
||||
func (roleMapping *RoleMapping) RoleNames() []string {
|
||||
if roleMapping == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
roleNames := make([]string, 0, len(roleMapping.GroupMappings)+1)
|
||||
|
||||
if roleMapping.DefaultRole != "" {
|
||||
seen[roleMapping.DefaultRole] = struct{}{}
|
||||
roleNames = append(roleNames, roleMapping.DefaultRole)
|
||||
if role, err := types.NewRole(strings.ToUpper(roleMapping.DefaultRole)); err == nil {
|
||||
return role
|
||||
}
|
||||
}
|
||||
|
||||
for _, roleName := range roleMapping.GroupMappings {
|
||||
if roleName == "" {
|
||||
continue
|
||||
}
|
||||
if _, duplicate := seen[roleName]; duplicate {
|
||||
continue
|
||||
}
|
||||
seen[roleName] = struct{}{}
|
||||
roleNames = append(roleNames, roleName)
|
||||
}
|
||||
|
||||
return roleNames
|
||||
return types.RoleViewer
|
||||
}
|
||||
|
||||
func compareRoles(a, b types.Role) int {
|
||||
order := map[types.Role]int{
|
||||
types.RoleViewer: 0,
|
||||
types.RoleEditor: 1,
|
||||
types.RoleAdmin: 2,
|
||||
}
|
||||
return order[a] - order[b]
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ var (
|
||||
ErrCodeRoleUnsupported = errors.MustNewCode("role_unsupported")
|
||||
ErrCodeRoleHasUserAssignees = errors.MustNewCode("role_has_user_assignees")
|
||||
ErrCodeRoleHasServiceAccountAssignees = errors.MustNewCode("role_has_service_account_assignees")
|
||||
ErrCodeRoleHasAuthDomainMappings = errors.MustNewCode("role_has_auth_domain_mappings")
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -81,8 +80,8 @@ type RoleWithTransactionGroups struct {
|
||||
|
||||
type PostableRole struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
Description string `json:"description" required:"true"`
|
||||
TransactionGroups TransactionGroups `json:"transactionGroups" required:"true" nullable:"false"`
|
||||
Description string `json:"description" required:"false"`
|
||||
TransactionGroups TransactionGroups `json:"transactionGroups" required:"false" nullable:"false"`
|
||||
}
|
||||
|
||||
type UpdatableRole struct {
|
||||
@@ -168,32 +167,40 @@ func (role *Role) ErrIfManaged() error {
|
||||
}
|
||||
|
||||
func (role *PostableRole) UnmarshalJSON(data []byte) error {
|
||||
type Alias PostableRole
|
||||
var temp Alias
|
||||
shadow := struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
TransactionGroups *json.RawMessage `json:"transactionGroups"`
|
||||
}{}
|
||||
|
||||
if err := json.Unmarshal(data, &temp); err != nil {
|
||||
if err := json.Unmarshal(data, &shadow); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if temp.Name == "" {
|
||||
if shadow.Name == "" {
|
||||
return errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "name is missing from the request")
|
||||
}
|
||||
|
||||
if match := roleNameRegex.MatchString(temp.Name); !match {
|
||||
if match := roleNameRegex.MatchString(shadow.Name); !match {
|
||||
return errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "name must contain only lowercase letters (a-z) and hyphens (-), and be at most 50 characters long.")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(temp.Name, managedRolePrefix) {
|
||||
if strings.HasPrefix(shadow.Name, managedRolePrefix) {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "role name cannot start with %q as it is reserved for SigNoz managed roles.", managedRolePrefix)
|
||||
}
|
||||
|
||||
if temp.TransactionGroups == nil {
|
||||
return errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "transactionGroups is required").WithAdditional("send an empty array to create a role with no transaction groups")
|
||||
var transactionGroups TransactionGroups
|
||||
if shadow.TransactionGroups != nil {
|
||||
var err error
|
||||
transactionGroups, err = NewTransactionGroups(*shadow.TransactionGroups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
role.Name = temp.Name
|
||||
role.Description = temp.Description
|
||||
role.TransactionGroups = temp.TransactionGroups
|
||||
role.Name = shadow.Name
|
||||
role.Description = shadow.Description
|
||||
role.TransactionGroups = transactionGroups
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -207,9 +214,6 @@ func (role *UpdatableRole) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// A pointer distinguishes an omitted/null description from an explicit empty string: the field
|
||||
// must be sent (update reconciles to exactly what is given), but an empty string is allowed so a
|
||||
// caller can deliberately clear the description.
|
||||
if shadow.Description == nil {
|
||||
return errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "description is required").WithAdditional("send an empty string to clear the description")
|
||||
}
|
||||
@@ -299,20 +303,6 @@ func MustGetSigNozManagedRoleFromExistingRole(role types.Role) string {
|
||||
return managedRole
|
||||
}
|
||||
|
||||
func NormalizeRoleName(role string) string {
|
||||
legacyRole, err := types.NewRole(strings.ToUpper(role))
|
||||
if err != nil {
|
||||
return role
|
||||
}
|
||||
|
||||
managedRole, ok := ExistingRoleToSigNozManagedRoleMap[legacyRole]
|
||||
if !ok {
|
||||
return role
|
||||
}
|
||||
|
||||
return managedRole
|
||||
}
|
||||
|
||||
type RoleStore interface {
|
||||
Create(context.Context, *Role) error
|
||||
Get(context.Context, valuer.UUID, valuer.UUID) (*Role, error)
|
||||
|
||||
@@ -3,10 +3,22 @@ package authtypes
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type rawTransactionGroup struct {
|
||||
Relation string `json:"relation"`
|
||||
ObjectGroup struct {
|
||||
Resource struct {
|
||||
Type string `json:"type"`
|
||||
Kind string `json:"kind"`
|
||||
} `json:"resource"`
|
||||
Selectors []string `json:"selectors"`
|
||||
} `json:"objectGroup"`
|
||||
}
|
||||
|
||||
type Transaction struct {
|
||||
ID valuer.UUID `json:"-"`
|
||||
Relation Relation `json:"relation" required:"true"`
|
||||
@@ -39,16 +51,23 @@ func NewTransaction(relation Relation, object coretypes.Object) (*Transaction, e
|
||||
return &Transaction{ID: valuer.GenerateUUID(), Relation: relation, Object: object}, nil
|
||||
}
|
||||
|
||||
func NewTransactionGroup(relation Relation, objectGroup coretypes.ObjectGroup) (*TransactionGroup, error) {
|
||||
if err := coretypes.ErrIfVerbNotValidForResource(relation.Verb, objectGroup.Resource); err != nil {
|
||||
return nil, err
|
||||
func NewTransactionGroups(data []byte) (TransactionGroups, error) {
|
||||
var rawGroups []rawTransactionGroup
|
||||
if err := json.Unmarshal(data, &rawGroups); err != nil {
|
||||
return nil, errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "transactionGroups must be an array of {relation, objectGroup} objects")
|
||||
}
|
||||
|
||||
if _, err := coretypes.NewObjectsFromObjectGroup(objectGroup); err != nil {
|
||||
return nil, err
|
||||
groups := make(TransactionGroups, 0, len(rawGroups))
|
||||
for index, rawGroup := range rawGroups {
|
||||
group, err := newTransactionGroup(rawGroup, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groups = append(groups, group)
|
||||
}
|
||||
|
||||
return &TransactionGroup{Relation: relation, ObjectGroup: objectGroup}, nil
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func NewGettableTransaction(results []*TransactionWithAuthorization) []*GettableTransaction {
|
||||
@@ -88,26 +107,6 @@ func (transaction *Transaction) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (transactionGroup *TransactionGroup) UnmarshalJSON(data []byte) error {
|
||||
var shadow = struct {
|
||||
Relation Relation
|
||||
ObjectGroup coretypes.ObjectGroup
|
||||
}{}
|
||||
|
||||
err := json.Unmarshal(data, &shadow)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
group, err := NewTransactionGroup(shadow.Relation, shadow.ObjectGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*transactionGroup = *group
|
||||
return nil
|
||||
}
|
||||
|
||||
func (transaction *Transaction) TransactionKey() string {
|
||||
return transaction.Relation.StringValue() + ":" + transaction.Object.Resource.Type.StringValue() + ":" + transaction.Object.Resource.Kind.String()
|
||||
}
|
||||
@@ -156,3 +155,39 @@ func (groups TransactionGroups) selectorSet() map[string]struct{} {
|
||||
func (group *TransactionGroup) selectorKey(selector coretypes.Selector) string {
|
||||
return group.Relation.StringValue() + "|" + group.ObjectGroup.Resource.String() + "|" + selector.String()
|
||||
}
|
||||
|
||||
func newTransactionGroup(raw rawTransactionGroup, index int) (*TransactionGroup, error) {
|
||||
verb, err := coretypes.NewVerb(raw.Relation)
|
||||
if err != nil {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "transactionGroups[%d].relation: %s", index, err.Error())
|
||||
}
|
||||
|
||||
resourceType, err := coretypes.NewType(raw.ObjectGroup.Resource.Type)
|
||||
if err != nil {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "transactionGroups[%d].objectGroup.resource.type: %s", index, err.Error())
|
||||
}
|
||||
|
||||
kind, err := coretypes.NewKind(raw.ObjectGroup.Resource.Kind)
|
||||
if err != nil {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "transactionGroups[%d].objectGroup.resource.kind: %s", index, err.Error())
|
||||
}
|
||||
|
||||
resourceRef := coretypes.ResourceRef{Type: resourceType, Kind: kind}
|
||||
if err := coretypes.ErrIfVerbNotValidForResource(verb, resourceRef); err != nil {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "transactionGroups[%d]: %s", index, err.Error())
|
||||
}
|
||||
|
||||
selectors := make([]coretypes.Selector, 0, len(raw.ObjectGroup.Selectors))
|
||||
for selectorIndex, rawSelector := range raw.ObjectGroup.Selectors {
|
||||
selector, err := resourceType.Selector(rawSelector)
|
||||
if err != nil {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "transactionGroups[%d].objectGroup.selectors[%d]: %s", index, selectorIndex, err.Error())
|
||||
}
|
||||
selectors = append(selectors, selector)
|
||||
}
|
||||
|
||||
return &TransactionGroup{
|
||||
Relation: Relation{Verb: verb},
|
||||
ObjectGroup: coretypes.ObjectGroup{Resource: resourceRef, Selectors: selectors},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -39,6 +39,48 @@ func MustNewKind(str string) Kind {
|
||||
return kind
|
||||
}
|
||||
|
||||
func (name Kind) Enum() []any {
|
||||
return []any{
|
||||
KindAnonymous,
|
||||
KindOrganization,
|
||||
KindRole,
|
||||
KindServiceAccount,
|
||||
KindUser,
|
||||
KindNotificationChannel,
|
||||
KindRoutePolicy,
|
||||
KindApdexSetting,
|
||||
KindAuthDomain,
|
||||
KindSession,
|
||||
KindCloudIntegration,
|
||||
KindCloudIntegrationService,
|
||||
KindIntegration,
|
||||
KindDashboard,
|
||||
KindPublicDashboard,
|
||||
KindIngestionKey,
|
||||
KindIngestionLimit,
|
||||
KindPipeline,
|
||||
KindUserPreference,
|
||||
KindOrgPreference,
|
||||
KindQuickFilter,
|
||||
KindTTLSetting,
|
||||
KindRule,
|
||||
KindPlannedMaintenance,
|
||||
KindSavedView,
|
||||
KindTraceFunnel,
|
||||
KindFactorPassword,
|
||||
KindFactorAPIKey,
|
||||
KindLicense,
|
||||
KindSubscription,
|
||||
KindLogs,
|
||||
KindTraces,
|
||||
KindMetrics,
|
||||
KindAuditLogs,
|
||||
KindMeterMetrics,
|
||||
KindLogsField,
|
||||
KindTracesField,
|
||||
}
|
||||
}
|
||||
|
||||
func (name Kind) String() string {
|
||||
return name.val
|
||||
}
|
||||
|
||||
83
pkg/types/inframonitoringtypes/checks.go
Normal file
83
pkg/types/inframonitoringtypes/checks.go
Normal file
@@ -0,0 +1,83 @@
|
||||
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"`
|
||||
}
|
||||
71
pkg/types/inframonitoringtypes/checks_constants.go
Normal file
71
pkg/types/inframonitoringtypes/checks_constants.go
Normal file
@@ -0,0 +1,71 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
110
pkg/types/inframonitoringtypes/checks_test.go
Normal file
110
pkg/types/inframonitoringtypes/checks_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
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,7 +12,6 @@ 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,7 +12,6 @@ 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,7 +12,6 @@ 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,7 +12,6 @@ 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"`
|
||||
}
|
||||
@@ -30,10 +29,6 @@ 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,7 +12,6 @@ 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,7 +12,6 @@ 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,7 +12,6 @@ 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,7 +12,6 @@ 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,7 +12,6 @@ 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,7 +12,6 @@ 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,7 +53,8 @@ var (
|
||||
LLMPricingRuleCacheModeSubtract = LLMPricingRuleCacheMode{valuer.NewString("subtract")}
|
||||
// LLMPricingRuleCacheModeAdditive: cached tokens are reported separately (Anthropic-style).
|
||||
LLMPricingRuleCacheModeAdditive = LLMPricingRuleCacheMode{valuer.NewString("additive")}
|
||||
// LLMPricingRuleCacheModeUnknown: provider behaviour is unknown; falls back to subtract.
|
||||
// LLMPricingRuleCacheModeUnknown: provider behaviour is unknown. buildProcessorConfig
|
||||
// normalizes this to an empty mode in the collector config.
|
||||
LLMPricingRuleCacheModeUnknown = LLMPricingRuleCacheMode{valuer.NewString("unknown")}
|
||||
)
|
||||
|
||||
|
||||
@@ -26,24 +26,23 @@ type LLMPricingRuleProcessorAttrs struct {
|
||||
CacheWrite string `yaml:"cache_write" json:"cache_write"`
|
||||
}
|
||||
|
||||
// LLMPricingRuleProcessorDefaultPricing holds the pricing unit and the list of model-specific rules.
|
||||
// LLMPricingRuleProcessorDefaultPricing holds 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" json:"cache"`
|
||||
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,omitempty" json:"cache,omitempty"`
|
||||
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" json:"mode"`
|
||||
Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`
|
||||
Read float64 `yaml:"read" json:"read"`
|
||||
Write float64 `yaml:"write" json:"write"`
|
||||
}
|
||||
@@ -61,10 +60,14 @@ 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 {
|
||||
cache = LLMPricingRuleProcessorCache{
|
||||
Mode: r.Pricing.Cache.Mode.StringValue(),
|
||||
mode := r.Pricing.Cache.Mode.StringValue()
|
||||
if mode != LLMPricingRuleCacheModeSubtract.StringValue() && mode != LLMPricingRuleCacheModeAdditive.StringValue() {
|
||||
mode = ""
|
||||
}
|
||||
cache = &LLMPricingRuleProcessorCache{
|
||||
Mode: mode,
|
||||
Read: r.Pricing.Cache.Read,
|
||||
Write: r.Pricing.Cache.Write,
|
||||
}
|
||||
@@ -87,7 +90,6 @@ func buildProcessorConfig(rules []*LLMPricingRule) *LLMPricingRuleProcessorConfi
|
||||
CacheWrite: GenAIUsageCacheCreationInputTokens,
|
||||
},
|
||||
DefaultPricing: LLMPricingRuleProcessorDefaultPricing{
|
||||
Unit: UnitPerMillionTokens.StringValue(),
|
||||
Rules: pricingRules,
|
||||
},
|
||||
OutputAttrs: LLMPricingRuleProcessorOutputAttrs{
|
||||
|
||||
@@ -41,6 +41,16 @@ 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
|
||||
@@ -62,6 +72,24 @@ 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,7 +11,6 @@ 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,7 +11,6 @@ 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
|
||||
|
||||
42
pkg/types/llmpricingruletypes/testdata/collector_rule_cache_mode_unknown.yaml
vendored
Normal file
42
pkg/types/llmpricingruletypes/testdata/collector_rule_cache_mode_unknown.yaml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
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
|
||||
39
pkg/types/llmpricingruletypes/testdata/collector_rule_without_cache.yaml
vendored
Normal file
39
pkg/types/llmpricingruletypes/testdata/collector_rule_without_cache.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
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,7 +11,6 @@ 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:
|
||||
|
||||
@@ -260,11 +260,27 @@ type MetricHighlightsResponse struct {
|
||||
ActiveTimeSeries uint64 `json:"activeTimeSeries" required:"true"`
|
||||
}
|
||||
|
||||
// MetricNameQuery represents the query parameters for endpoints that take a metric name.
|
||||
type MetricNameQuery struct {
|
||||
MetricName string `query:"metricName" required:"true" description:"The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies)."`
|
||||
}
|
||||
|
||||
// Validate ensures MetricNameQuery contains acceptable values.
|
||||
func (q *MetricNameQuery) Validate() error {
|
||||
if q == nil {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
|
||||
}
|
||||
if q.MetricName == "" {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MetricAttributesRequest represents the query parameters for the metric attributes endpoint.
|
||||
type MetricAttributesRequest struct {
|
||||
MetricName string `json:"-"`
|
||||
Start *int64 `query:"start"`
|
||||
End *int64 `query:"end"`
|
||||
MetricName string `query:"metricName" required:"true" description:"The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies)."`
|
||||
Start *int64 `query:"start" description:"Start of the time range as a Unix timestamp in milliseconds."`
|
||||
End *int64 `query:"end" description:"End of the time range as a Unix timestamp in milliseconds."`
|
||||
}
|
||||
|
||||
// Validate ensures MetricAttributesRequest contains acceptable values.
|
||||
@@ -273,6 +289,10 @@ func (req *MetricAttributesRequest) Validate() error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
|
||||
}
|
||||
|
||||
if req.MetricName == "" {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName is required")
|
||||
}
|
||||
|
||||
if req.Start != nil && req.End != nil {
|
||||
if *req.Start >= *req.End {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "start (%d) must be less than end (%d)", *req.Start, *req.End)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{"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}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user