mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-21 17:40:44 +01:00
Compare commits
6 Commits
test/no-ra
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e69d3fcc8c | ||
|
|
354214ca87 | ||
|
|
e3488c1592 | ||
|
|
574bfa62b8 | ||
|
|
ba6c4f83a6 | ||
|
|
ada6a20ea2 |
164
.claude/skills/sbshot-visual-diff/SKILL.md
Normal file
164
.claude/skills/sbshot-visual-diff/SKILL.md
Normal file
@@ -0,0 +1,164 @@
|
||||
---
|
||||
name: sbshot-visual-diff
|
||||
description: Shoot Storybook stories with sbshot at two git refs and produce a diff a reviewer can read. Use when asked for a visual diff, before/after screenshots, a screenshot comparison for a PR, or to prove a change did not alter screens it should not have.
|
||||
---
|
||||
|
||||
# sbshot visual diffs
|
||||
|
||||
`npx -y github:SigNoz/sbshot` shoots Storybook stories and pixel-diffs two runs.
|
||||
Use it. Do not hand-roll a Playwright screenshot loop or a canvas pixel-compare:
|
||||
sbshot already handles story selection, themes, settling, retries, diffing,
|
||||
tagging, a viewer and a static export, and its runs are the format the rest of
|
||||
the team can open.
|
||||
|
||||
sbshot is not installed by any package. It is run through `npx`, so nothing it
|
||||
knows travels into this repo on its own. This file is where that knowledge
|
||||
lives.
|
||||
|
||||
## The shape of a run
|
||||
|
||||
```
|
||||
capture <storybook> --name-run <ref> # once per git ref
|
||||
diff <baseline> <after> # pairs them by relative path
|
||||
tag <diff> <tag> <story-id>... # what the reviewer should look at
|
||||
export <diff> --out <dir> # a static site, hostable
|
||||
```
|
||||
|
||||
`<storybook>` is a running dev server (`http://localhost:6006`), a build
|
||||
directory holding `index.json`, or a project directory with `.storybook/`. The
|
||||
frontend has **no `build-storybook` script**, only `storybook dev -p 6006`, so
|
||||
point `capture` at a dev server.
|
||||
|
||||
## Cover the change with stories first
|
||||
|
||||
**A diff can only show what a story renders.** A screen, state or variant with no
|
||||
story is invisible to sbshot: it produces no pair, so it reads as "nothing
|
||||
changed" rather than "never looked at". This is the failure mode that makes a
|
||||
clean-looking diff worthless.
|
||||
|
||||
Before capturing, list the screens and states the change touches and check each
|
||||
has a story. For anything missing, write it first — the `signoz-page-story`
|
||||
skill covers how. New states reached only by an interaction need a `play`
|
||||
function, or the capture shoots the page at rest and the state never appears.
|
||||
|
||||
Then confirm it worked: the two runs' story counts should differ by exactly the
|
||||
stories the change adds. A new screen that produced no new story id is a screen
|
||||
nobody is reviewing. Say the count in the summary, so a reader can tell coverage
|
||||
from silence.
|
||||
|
||||
## Rules that are not optional
|
||||
|
||||
**One freshly started Storybook per ref.** Never start one server and switch git
|
||||
branches under it. Vite's HMR reloads the page, but some modules do not
|
||||
re-initialise, so the shot differs silently from a clean boot and the difference
|
||||
looks like a real change. Settle time does not fix this and is not the variable.
|
||||
Use one worktree and one port per ref, and stop each server before the next.
|
||||
|
||||
**Capture both themes: `--theme dark,light`.** Without it sbshot shoots the
|
||||
story's own default and everything lands in a single `default/` folder, so every
|
||||
regression in the other theme is invisible. Themes double the shot count, so
|
||||
scope with `--stories` or `--title`, never by dropping a theme.
|
||||
|
||||
**Baseline is the merge-base, not `origin/main`.** `git merge-base origin/main
|
||||
<branch>`. Against a moved main, every commit main gained since the branch's base
|
||||
shows up reversed, as though the branch reverted it.
|
||||
|
||||
**The percentage sorts the list, it does not tell you what matters.** A dialog
|
||||
that dims the page scores high for a one-line change, and a whole new form
|
||||
section can score low. Open the shots and say what changed on each screen. The
|
||||
biggest number is usually the least interesting one.
|
||||
|
||||
**If you cannot explain a pair, assume the capture is at fault, not the code.**
|
||||
Open it, crop both sides and compare them before calling it noise. A pixel count
|
||||
and a good guess are not evidence.
|
||||
|
||||
## Noise, and the floor that proves it
|
||||
|
||||
The same story does not always render identically twice. Pairs often move for
|
||||
reasons that have nothing to do with the change:
|
||||
|
||||
- a focus ring left on whatever a `play` function clicked last
|
||||
- a scrollbar appearing, or its thumb resting a few pixels off
|
||||
- a toast, tooltip or dialog caught mid-fade
|
||||
- a text caret blinking, or a selection highlight
|
||||
- an animation or spinner at a different frame
|
||||
- relative timestamps ("2 days ago") crossing a boundary
|
||||
- hover or active styling from where the pointer happened to rest
|
||||
- a late-loading font or image shifting a line of text
|
||||
|
||||
Do not argue about these one at a time. **Measure them**: shoot the same commit
|
||||
twice and diff those two runs against each other. Anything that moves there moved
|
||||
for no reason, so it is noise.
|
||||
|
||||
```
|
||||
capture <storybook> --name-run noise-a
|
||||
capture <storybook> --name-run noise-b # same tree, its own fresh server
|
||||
diff noise-a noise-b --name-diff noise
|
||||
diff <baseline> <after> --noise noise # marks the movers flaky
|
||||
```
|
||||
|
||||
**The noise run has to cover the same stories as the real diff.** Do not scope it
|
||||
down to the few pairs that moved, however tempting that is. `--noise` marks a
|
||||
pair flaky when the noise diff does not show it as unchanged, so every story the
|
||||
noise run leaves out is marked flaky too, including the new screens the reviewer
|
||||
is there to look at. Shooting two narrow runs and diffing those against each
|
||||
other has the same problem. Either shoot the full set twice, or skip `--noise`
|
||||
and explain the few movers by hand.
|
||||
|
||||
That is the trade: a second full capture costs about as long as the first, so on
|
||||
a small diff it is often quicker to crop the two or three movers and say what
|
||||
each one is. Use `--noise` when the diff is large, or when wiring `--fail-over`
|
||||
into CI, where something has to decide without a person looking.
|
||||
|
||||
If a pair is flaky every run and the state it captures matters, fix the story
|
||||
rather than living with it: blur what the `play` clicked, freeze the clock, or
|
||||
assert the state instead of shooting it.
|
||||
|
||||
## Making it readable for a reviewer
|
||||
|
||||
A reviewer cannot open `localhost`, so `sbshot ui` is for you, not for them.
|
||||
|
||||
1. `tag` the pairs that matter, with `--note` saying why. The viewer filters by
|
||||
tag, so the reviewer lands on the real changes instead of scrolling past
|
||||
noise that scored higher.
|
||||
2. `export <diff> --out <dir>`. It writes a static site carrying only the pairs
|
||||
that are not identical, converted to WebP, so a large diff stays small.
|
||||
3. Host it. A Claude artifact works: publish the export's files and give the
|
||||
reviewer the link. `index.html` is reserved for the artifact's own page, so
|
||||
publish sbshot's entry as `viewer.html` and link to
|
||||
`viewer.html#/job/diff/<diff>`.
|
||||
4. Put the link in the PR body under **Screenshots / Screen Recordings**.
|
||||
|
||||
Say the numbers plainly: how many screens changed, how many are new, how many are
|
||||
pixel-identical, how many are flaky. The count of unchanged screens is often the
|
||||
most useful claim in the diff, because it is the one a reviewer cannot check by
|
||||
eye.
|
||||
|
||||
## Operational traps
|
||||
|
||||
- A full capture is hundreds of shots. Scope with `--title "Pages/Alerts"` or
|
||||
`--stories "a,b,c"` (substring match on id or Title/Name, repeatable).
|
||||
- A fresh worktree cannot build Storybook: `i18n-translations-hash.json` is
|
||||
gitignored and generated. Run `node ./i18-generate-hash.cjs` in
|
||||
`<worktree>/frontend`, and symlink `node_modules` from the main checkout.
|
||||
- `pnpm test:storybook` starts its own server on 6006 and hangs silently if
|
||||
something already holds that port. Free it first.
|
||||
- ImageMagick on macOS may have no default font, so `--mode green-parallel` fails
|
||||
with ``unable to read font `' ``. Map Helvetica to a real file in
|
||||
`~/.config/ImageMagick/type.xml`.
|
||||
- `sbshot link <dir> <name>` adds a run from elsewhere into the workspace, but it
|
||||
symlinks. Keep the source directory alive until you have exported.
|
||||
- The export directory is build output. Delete it when done, or gitignore it.
|
||||
- Running the whole Storybook test suite locally fails broadly with
|
||||
`page.goto: Timeout` from resource starvation. Scope it; CI is the real signal.
|
||||
|
||||
## Done means
|
||||
|
||||
- [ ] every screen and state the change touches has a story, written before capturing
|
||||
- [ ] one capture per ref, each from its own freshly started Storybook
|
||||
- [ ] `--theme dark,light` on every capture
|
||||
- [ ] baseline is the merge-base
|
||||
- [ ] noise either measured with a full second capture, or each mover explained by hand
|
||||
- [ ] every non-flaky pair opened and explained, not inferred from its size
|
||||
- [ ] the pairs that matter tagged, with a note
|
||||
- [ ] exported and hosted, and the link in the PR body
|
||||
@@ -151,9 +151,42 @@ that only exists on a trial needs the plan too:
|
||||
`args: { plan: 'on-trial', revoked: ['create:subscription'] }`. A permission
|
||||
whose denial changes nothing on screen gets no story: say so in the PR.
|
||||
|
||||
Verify these by their disabled states, not their text. The page reads the same
|
||||
either way, so a story that is wrong looks right: read `disabled` off the buttons
|
||||
the permission gates, and check the denial callout is there or gone.
|
||||
Check these by what is turned off, not by what the page says. The page reads the
|
||||
same either way, so a story that is wrong still looks right. Check that the
|
||||
control the permission guards is off, and that the denial message is there or
|
||||
gone.
|
||||
|
||||
**Do not use `toBeEnabled` or `toBeDisabled`.** They only look at the `disabled`
|
||||
attribute. `@signozhq/ui` turns a button off with `aria-disabled` instead, so
|
||||
these matchers pass on a button nobody can click, and the test goes green while
|
||||
clicking straight through a denial. Check the attribute the component really
|
||||
sets:
|
||||
|
||||
```ts
|
||||
expect(save).toHaveAttribute('aria-disabled', 'true');
|
||||
```
|
||||
|
||||
**Some controls are off with no attribute at all.** The browser turns off
|
||||
everything inside a disabled `<fieldset>` without marking it, and a switch is a
|
||||
`<button>` underneath. For those, an attribute check proves nothing, and so does
|
||||
looking for a parent `fieldset[disabled]`. Click it and check nothing happened:
|
||||
|
||||
```ts
|
||||
const before = toggle.getAttribute('aria-checked');
|
||||
await userEvent.click(toggle);
|
||||
await expect(toggle.getAttribute('aria-checked')).toBe(before);
|
||||
```
|
||||
|
||||
**Check that a new assertion can fail.** Flip it once and run it. If that story
|
||||
goes red and nothing else does, the assertion is real; put it back. A test that
|
||||
cannot fail is worse than no test, because it looks like coverage.
|
||||
|
||||
**Delete a story once its difference disappears.** Say a story checks that a
|
||||
button reads "View" rather than "Edit" when a permission is missing. If the label
|
||||
later becomes the same either way, that story passes no matter which permissions
|
||||
are held, including all of them, and it is testing nothing. When a behaviour
|
||||
stops depending on a permission, delete the story or rewrite it to lock in the
|
||||
new behaviour, and say which one you did in the PR.
|
||||
|
||||
## Rules
|
||||
|
||||
|
||||
@@ -63,6 +63,10 @@ not catch an unstyled page. Take them with
|
||||
[sbshot](https://github.com/SigNoz/sbshot), e.g.
|
||||
`sbshot capture http://localhost:6006 --title <story title> --theme dark,light`.
|
||||
|
||||
That is one run, for checking the story you just wrote. Comparing two branches is
|
||||
a different job: see the `sbshot-visual-diff` skill, which covers the baseline to
|
||||
pick, measuring the noise and exporting something a reviewer can open.
|
||||
|
||||
## Gates
|
||||
|
||||
- **Console silent.** `[storybook] no msw handler`, a 501 from the catch-all, an
|
||||
|
||||
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -75,6 +75,7 @@ jobs:
|
||||
- ttl
|
||||
- clickhousecluster
|
||||
- metricreduction
|
||||
- metricsexplorer
|
||||
sqlstore-provider:
|
||||
- postgres
|
||||
- sqlite
|
||||
|
||||
@@ -41,7 +41,6 @@ linters:
|
||||
no-global: all
|
||||
context: all
|
||||
static-msg: true
|
||||
key-naming-case: snake
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
|
||||
@@ -3581,6 +3581,7 @@ components:
|
||||
- meter-metrics
|
||||
- logs-field
|
||||
- traces-field
|
||||
- metrics-field
|
||||
type: string
|
||||
CoretypesObject:
|
||||
properties:
|
||||
@@ -20637,9 +20638,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- metrics-field:list
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- metrics-field:list
|
||||
summary: List metric names
|
||||
tags:
|
||||
- metrics
|
||||
@@ -20706,9 +20707,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- rule:list
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- rule:list
|
||||
summary: Get metric alerts
|
||||
tags:
|
||||
- metrics
|
||||
@@ -20790,9 +20791,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- metrics-field:read
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- metrics-field:read
|
||||
summary: Get metric attributes
|
||||
tags:
|
||||
- metrics
|
||||
@@ -20860,9 +20861,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- metrics:read
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- metrics:read
|
||||
summary: Get metric highlights
|
||||
tags:
|
||||
- metrics
|
||||
@@ -20919,9 +20920,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- metrics:read
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- metrics:read
|
||||
summary: Inspect raw metric data points
|
||||
tags:
|
||||
- metrics
|
||||
@@ -20989,9 +20990,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- metrics-field:read
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- metrics-field:read
|
||||
summary: Get metric metadata
|
||||
tags:
|
||||
- metrics
|
||||
@@ -21035,9 +21036,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- metrics-field:update
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
- metrics-field:update
|
||||
summary: Update metric metadata
|
||||
tags:
|
||||
- metrics
|
||||
@@ -21083,9 +21084,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- metrics:read
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- metrics:read
|
||||
summary: Check if non-SigNoz metrics have been received
|
||||
tags:
|
||||
- metrics
|
||||
@@ -21142,9 +21143,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- metrics:read
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- metrics:read
|
||||
summary: Get metrics statistics
|
||||
tags:
|
||||
- metrics
|
||||
@@ -21201,9 +21202,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- metrics:read
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- metrics:read
|
||||
summary: Get metrics treemap
|
||||
tags:
|
||||
- metrics
|
||||
@@ -28342,9 +28343,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- dashboard:list
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- dashboard:list
|
||||
summary: Get metric dashboards (v2)
|
||||
tags:
|
||||
- metrics
|
||||
|
||||
@@ -43,6 +43,25 @@ type Resource interface {
|
||||
|
||||
All resources are registered in [pkg/types/coretypes/registry_resource.go](/pkg/types/coretypes/registry_resource.go) using constructors like `NewResourceMetaResource(KindDashboard)` or `NewResourceTelemetryResource(KindLogs)`.
|
||||
|
||||
### Telemetry resources and metaresources
|
||||
|
||||
Almost every feature resource is one of these two types, and they answer different questions.
|
||||
|
||||
- A **telemetry resource** (`logs`, `traces`, `metrics`, `meter-metrics`, `audit-logs`) is the data itself. It only supports `read`. Its selector is not an ID but the query: the route wires a `TelemetryResourceDef` whose extractor turns the caller's filter into `<query_type>/<key>/<value>` IDs, and the check ladders each one through `<query_type>/<key>/*`, `<query_type>/*` and `*`. A grant such as `builder_query/signoz.workspace.key.id/key-a` therefore decides which rows a role may read; a query that does not pin a granted key resolves to `<query_type>/*` and is denied for key-scoped roles. Managed roles hold `*`.
|
||||
- A **metaresource** (`dashboard`, `rule`, `logs-field`, ...) is an object that describes or configures the system. It supports whichever of `create`, `read`, `update`, `delete` and `list` the resource declares, its selectors are object IDs or `*`, and routes wire a `BasicResourceDef`. It says nothing about rows: `dashboard:read` on `*` does not grant the telemetry a dashboard's panels query.
|
||||
|
||||
The metrics explorer uses both on one surface:
|
||||
|
||||
| Route | Resource | Why |
|
||||
|---|---|---|
|
||||
| `POST /api/v2/metrics/stats` | `metrics:read` (telemetry) | Returns sample and series counts, i.e. the data. The selector comes from `filter.expression`, so a role scoped to a workspace key sees only its own samples. |
|
||||
| `GET /api/v2/metrics` | `metrics-field:list` (meta) | Lists metric names, types and units: the catalogue, not the samples. |
|
||||
| `POST /api/v2/metrics/metadata` | `metrics-field:update` (meta) | Edits a metric's type, unit and description. |
|
||||
|
||||
The two are independent: a role holding `metrics-field` `list` and `read` can browse the catalogue but cannot read a sample, and a role scoped to `builder_query/signoz.workspace.key.id/key-a` on `metrics` can query its samples but cannot list the catalogue.
|
||||
|
||||
For a new route, ask what it returns. Rows or aggregates over them go on the signal's telemetry resource with an extractor; anything that names, describes or configures telemetry goes on a metaresource.
|
||||
|
||||
### Selector
|
||||
|
||||
A `Selector` identifies *which* instance(s) of a resource a check is about — a UUID, a role name, or the wildcard `*`. A `SelectorFunc` maps the extracted resource ID to selectors at request time. Two prebuilt ones cover most routes ([pkg/types/coretypes/selector.go](/pkg/types/coretypes/selector.go)):
|
||||
@@ -142,6 +161,7 @@ Because both paths go through the same middleware and the same `ResourceDef` dec
|
||||
## What should I remember?
|
||||
|
||||
- Declare authz in the registries ([pkg/types/coretypes](/pkg/types/coretypes)), not in migrations — tuples for new organizations are derived from code at bootstrap.
|
||||
- Telemetry resources are the data: `read` only, selector derived from the query. Metaresources are objects about the data: CRUD and list, ID or wildcard selectors. A route that returns rows or aggregates is telemetry; one that names or configures them is meta, as `metrics` versus `metrics-field` shows.
|
||||
- A new kind never needs an OpenFGA schema change; only a new type does, and then **both** [pkg/authz/openfgaschema/base.fga](/pkg/authz/openfgaschema/base.fga) and [ee/authz/openfgaschema/base.fga](/ee/authz/openfgaschema/base.fga) must be updated together.
|
||||
- Prefer `CheckResources` + `ResourceDef` over the coarse `ViewAccess`/`EditAccess`/`AdminAccess` gates for new routes.
|
||||
- Use `WildcardSelector` for `create`/`list`, `IDSelector` for instance operations, and a custom `SelectorFunc` when the request ID is not the FGA selector.
|
||||
|
||||
@@ -116,7 +116,7 @@ func (c *clickhouse) EstimateCardinality(ctx context.Context, metricName string,
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
reducedExpr := "1"
|
||||
reducedExpr := "toUInt64(1)"
|
||||
if len(keptLabels) > 0 {
|
||||
reducedExpr = "uniq(("
|
||||
for i, label := range keptLabels {
|
||||
|
||||
@@ -3291,6 +3291,7 @@ export enum CoretypesKindDTO {
|
||||
'meter-metrics' = 'meter-metrics',
|
||||
'logs-field' = 'logs-field',
|
||||
'traces-field' = 'traces-field',
|
||||
'metrics-field' = 'metrics-field',
|
||||
}
|
||||
export enum CoretypesTypeDTO {
|
||||
user = 'user',
|
||||
|
||||
@@ -2,8 +2,11 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, InputNumber, Popover, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { LogViewMode } from 'container/OptionsMenu/types';
|
||||
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||
import {
|
||||
LogViewMode,
|
||||
FontSize,
|
||||
OptionsMenuConfig,
|
||||
} from 'container/OptionsMenu/types';
|
||||
import {
|
||||
Check,
|
||||
ChevronLeft,
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
"audit-logs",
|
||||
"meter-metrics",
|
||||
"logs-field",
|
||||
"traces-field"
|
||||
"traces-field",
|
||||
"metrics-field"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -4,14 +4,16 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metricsexplorertypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/metrics", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.ListMetrics),
|
||||
provider.authzMiddleware.CheckResources(provider.metricsExplorerHandler.ListMetrics, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListMetrics",
|
||||
Tags: []string{"metrics"},
|
||||
@@ -25,13 +27,18 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceMetricsField.Scope(coretypes.VerbList)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceMetricsField,
|
||||
Verb: coretypes.VerbList,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}))).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/stats", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetStats),
|
||||
provider.authzMiddleware.CheckResources(provider.metricsExplorerHandler.GetStats, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricsStats",
|
||||
Tags: []string{"metrics"},
|
||||
@@ -44,13 +51,18 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
|
||||
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: querybuilder.TelemetrySelector,
|
||||
Resources: querybuilder.BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceMetrics, "filter.expression"),
|
||||
}))).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/treemap", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetTreemap),
|
||||
provider.authzMiddleware.CheckResources(provider.metricsExplorerHandler.GetTreemap, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricsTreemap",
|
||||
Tags: []string{"metrics"},
|
||||
@@ -63,13 +75,18 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
|
||||
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: querybuilder.TelemetrySelector,
|
||||
Resources: querybuilder.BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceMetrics, "filter.expression"),
|
||||
}))).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/attributes", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricAttributes),
|
||||
provider.authzMiddleware.CheckResources(provider.metricsExplorerHandler.GetMetricAttributes, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricAttributes",
|
||||
Tags: []string{"metrics"},
|
||||
@@ -83,13 +100,18 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceMetricsField.Scope(coretypes.VerbRead)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceMetricsField,
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}))).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/metadata", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricMetadata),
|
||||
provider.authzMiddleware.CheckResources(provider.metricsExplorerHandler.GetMetricMetadata, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricMetadata",
|
||||
Tags: []string{"metrics"},
|
||||
@@ -103,13 +125,18 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceMetricsField.Scope(coretypes.VerbRead)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceMetricsField,
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}))).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/metadata", handler.New(
|
||||
provider.authzMiddleware.EditAccess(provider.metricsExplorerHandler.UpdateMetricMetadata),
|
||||
provider.authzMiddleware.CheckResources(provider.metricsExplorerHandler.UpdateMetricMetadata, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateMetricMetadata",
|
||||
Tags: []string{"metrics"},
|
||||
@@ -122,13 +149,18 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceMetricsField.Scope(coretypes.VerbUpdate)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceMetricsField,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: coretypes.ActionCategoryConfigurationChange,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}))).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/highlights", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricHighlights),
|
||||
provider.authzMiddleware.CheckResources(provider.metricsExplorerHandler.GetMetricHighlights, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricHighlights",
|
||||
Tags: []string{"metrics"},
|
||||
@@ -142,13 +174,18 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
|
||||
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: querybuilder.TelemetrySelector,
|
||||
Resources: querybuilder.BuilderQueryWildcardResources(coretypes.ResourceTelemetryResourceMetrics),
|
||||
}))).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/alerts", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricAlerts),
|
||||
provider.authzMiddleware.CheckResources(provider.metricsExplorerHandler.GetMetricAlerts, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricAlerts",
|
||||
Tags: []string{"metrics"},
|
||||
@@ -162,13 +199,18 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceRule.Scope(coretypes.VerbList)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceRule,
|
||||
Verb: coretypes.VerbList,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}))).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v3/metrics/dashboards", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricDashboardsV2),
|
||||
provider.authzMiddleware.CheckResources(provider.metricsExplorerHandler.GetMetricDashboardsV2, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricDashboardsV2",
|
||||
Tags: []string{"metrics"},
|
||||
@@ -182,13 +224,18 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceDashboard.Scope(coretypes.VerbList)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceDashboard,
|
||||
Verb: coretypes.VerbList,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}))).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/inspect", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.InspectMetrics),
|
||||
provider.authzMiddleware.CheckResources(provider.metricsExplorerHandler.InspectMetrics, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "InspectMetrics",
|
||||
Tags: []string{"metrics"},
|
||||
@@ -201,13 +248,18 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
|
||||
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: querybuilder.TelemetrySelector,
|
||||
Resources: querybuilder.BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceMetrics, "filter.expression"),
|
||||
}))).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/onboarding", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetOnboardingStatus),
|
||||
provider.authzMiddleware.CheckResources(provider.metricsExplorerHandler.GetOnboardingStatus, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricsOnboardingStatus",
|
||||
Tags: []string{"metrics"},
|
||||
@@ -220,8 +272,13 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusUnauthorized, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
|
||||
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: querybuilder.TelemetrySelector,
|
||||
Resources: querybuilder.BuilderQueryWildcardResources(coretypes.ResourceTelemetryResourceMetrics),
|
||||
}))).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -118,10 +118,47 @@
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
"logs": [
|
||||
{
|
||||
"name": "Project",
|
||||
"path": "gcp.project",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Resource Type",
|
||||
"path": "gcp.resource_type",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"gcp": {}
|
||||
"gcp": {
|
||||
"metrics": {
|
||||
"metricNames": [
|
||||
"cloudsql.googleapis.com/database/up",
|
||||
"cloudsql.googleapis.com/database/instance_state",
|
||||
"cloudsql.googleapis.com/database/replication/replica_lag",
|
||||
"cloudsql.googleapis.com/database/cpu/utilization",
|
||||
"cloudsql.googleapis.com/database/memory/utilization",
|
||||
"cloudsql.googleapis.com/database/disk/utilization",
|
||||
"cloudsql.googleapis.com/database/network/connections",
|
||||
"cloudsql.googleapis.com/database/mysql/queries",
|
||||
"cloudsql.googleapis.com/database/mysql/dml_operations_count",
|
||||
"cloudsql.googleapis.com/database/mysql/threads",
|
||||
"cloudsql.googleapis.com/database/mysql/innodb/buffer_pool_reads_count",
|
||||
"cloudsql.googleapis.com/database/mysql/innodb/buffer_pool_read_requests_count",
|
||||
"cloudsql.googleapis.com/database/mysql/slow_queries_count",
|
||||
"cloudsql.googleapis.com/database/mysql/aborted_connects_count",
|
||||
"cloudsql.googleapis.com/database/mysql/innodb/deadlocks_count",
|
||||
"cloudsql.googleapis.com/database/mysql/innodb/row_lock_waits_count",
|
||||
"cloudsql.googleapis.com/database/disk/read_ops_count",
|
||||
"cloudsql.googleapis.com/database/disk/write_ops_count"
|
||||
]
|
||||
},
|
||||
"logs": {
|
||||
"sinkFilter": "resource.type=\"cloudsql_database\" AND (logName:\"/logs/cloudsql.googleapis.com%2Fmysql\" OR (log_id(\"cloudaudit.googleapis.com/data_access\") AND protoPayload.request.@type=\"type.googleapis.com/google.cloud.sql.audit.v1.MysqlAuditEntry\"))"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
@@ -75,6 +75,12 @@
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/postgresql/insights/aggregate/io_time",
|
||||
"unit": "Microseconds",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/postgresql/insights/perquery/execution_time",
|
||||
"unit": "Microseconds",
|
||||
@@ -88,10 +94,43 @@
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
"logs": [
|
||||
{
|
||||
"name": "Project",
|
||||
"path": "gcp.project",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Resource Type",
|
||||
"path": "gcp.resource_type",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"gcp": {}
|
||||
"gcp": {
|
||||
"metrics": {
|
||||
"metricNames": [
|
||||
"cloudsql.googleapis.com/database/up",
|
||||
"cloudsql.googleapis.com/database/cpu/utilization",
|
||||
"cloudsql.googleapis.com/database/memory/utilization",
|
||||
"cloudsql.googleapis.com/database/memory/usage",
|
||||
"cloudsql.googleapis.com/database/disk/bytes_used",
|
||||
"cloudsql.googleapis.com/database/postgresql/num_backends",
|
||||
"cloudsql.googleapis.com/database/postgresql/num_backends_by_state",
|
||||
"cloudsql.googleapis.com/database/postgresql/transaction_count",
|
||||
"cloudsql.googleapis.com/database/postgresql/deadlock_count",
|
||||
"cloudsql.googleapis.com/database/postgresql/vacuum/oldest_transaction_age",
|
||||
"cloudsql.googleapis.com/database/postgresql/insights/aggregate/execution_time",
|
||||
"cloudsql.googleapis.com/database/postgresql/insights/aggregate/io_time",
|
||||
"cloudsql.googleapis.com/database/postgresql/insights/perquery/execution_time",
|
||||
"cloudsql.googleapis.com/database/postgresql/replication/replica_byte_lag"
|
||||
]
|
||||
},
|
||||
"logs": {
|
||||
"sinkFilter": "resource.type=\"cloudsql_database\" AND (logName:\"/logs/cloudsql.googleapis.com%2Fpostgres\" OR (log_id(\"cloudaudit.googleapis.com/data_access\") AND protoPayload.request.@type=\"type.googleapis.com/google.cloud.sql.audit.v1.PgAuditEntry\"))"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
@@ -52,10 +52,36 @@
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
"logs": [
|
||||
{
|
||||
"name": "Project",
|
||||
"path": "gcp.project",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Resource Type",
|
||||
"path": "gcp.resource_type",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"gcp": {}
|
||||
"gcp": {
|
||||
"metrics": {
|
||||
"metricNames": [
|
||||
"storage.googleapis.com/storage/v2/total_bytes",
|
||||
"storage.googleapis.com/storage/v2/total_count",
|
||||
"storage.googleapis.com/storage/v2/deleted_bytes",
|
||||
"storage.googleapis.com/api/request_count",
|
||||
"storage.googleapis.com/network/received_bytes_count",
|
||||
"storage.googleapis.com/network/sent_bytes_count",
|
||||
"storage.googleapis.com/authn/authentication_count"
|
||||
]
|
||||
},
|
||||
"logs": {
|
||||
"sinkFilter": "resource.type=\"gcs_bucket\""
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
@@ -106,10 +106,45 @@
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
"logs": [
|
||||
{
|
||||
"name": "Project",
|
||||
"path": "gcp.project",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Resource Type",
|
||||
"path": "gcp.resource_type",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"gcp": {}
|
||||
"gcp": {
|
||||
"metrics": {
|
||||
"metricNames": [
|
||||
"compute.googleapis.com/instance/uptime_total",
|
||||
"compute.googleapis.com/instance/cpu/utilization",
|
||||
"compute.googleapis.com/guest/memory/bytes_used",
|
||||
"compute.googleapis.com/guest/memory/percent_used",
|
||||
"agent.googleapis.com/memory/percent_used",
|
||||
"compute.googleapis.com/instance/disk/average_io_latency",
|
||||
"compute.googleapis.com/instance/disk/read_bytes_count",
|
||||
"compute.googleapis.com/instance/disk/write_bytes_count",
|
||||
"compute.googleapis.com/instance/disk/read_ops_count",
|
||||
"compute.googleapis.com/instance/disk/write_ops_count",
|
||||
"compute.googleapis.com/instance/disk/performance_status",
|
||||
"compute.googleapis.com/instance/network/received_bytes_count",
|
||||
"compute.googleapis.com/instance/network/sent_bytes_count",
|
||||
"compute.googleapis.com/instance/network/received_packets_count",
|
||||
"compute.googleapis.com/instance/network/sent_packets_count",
|
||||
"compute.googleapis.com/firewall/dropped_packets_count"
|
||||
]
|
||||
},
|
||||
"logs": {
|
||||
"sinkFilter": "resource.type=\"gce_instance\" OR protoPayload.serviceName=\"compute.googleapis.com\""
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
@@ -106,10 +106,45 @@
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
"logs": [
|
||||
{
|
||||
"name": "Project",
|
||||
"path": "gcp.project",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Resource Type",
|
||||
"path": "gcp.resource_type",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"gcp": {}
|
||||
"gcp": {
|
||||
"metrics": {
|
||||
"metricNames": [
|
||||
"kubernetes.io/container/cpu/limit_utilization",
|
||||
"kubernetes.io/container/cpu/request_utilization",
|
||||
"kubernetes.io/container/memory/limit_utilization",
|
||||
"kubernetes.io/container/memory/request_utilization",
|
||||
"kubernetes.io/container/restart_count",
|
||||
"kubernetes.io/pod/latencies/pod_first_ready",
|
||||
"kubernetes.io/pod/network/received_bytes_count",
|
||||
"kubernetes.io/pod/network/sent_bytes_count",
|
||||
"kubernetes.io/pod/volume/utilization",
|
||||
"kubernetes.io/node/cpu/allocatable_utilization",
|
||||
"kubernetes.io/node/memory/allocatable_utilization",
|
||||
"kubernetes.io/node/ephemeral_storage/used_bytes",
|
||||
"kubernetes.io/node/ephemeral_storage/allocatable_bytes",
|
||||
"kubernetes.io/node/network/received_bytes_count",
|
||||
"kubernetes.io/node/network/sent_bytes_count",
|
||||
"kubernetes.io/node/status_condition"
|
||||
]
|
||||
},
|
||||
"logs": {
|
||||
"sinkFilter": "resource.type=(\"k8s_container\" OR \"k8s_pod\" OR \"k8s_node\" OR \"k8s_cluster\" OR \"k8s_control_plane_component\" OR \"gke_cluster\" OR \"gke_nodepool\")"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
@@ -76,10 +76,40 @@
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
"logs": [
|
||||
{
|
||||
"name": "Project",
|
||||
"path": "gcp.project",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Resource Type",
|
||||
"path": "gcp.resource_type",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"gcp": {}
|
||||
"gcp": {
|
||||
"metrics": {
|
||||
"metricNames": [
|
||||
"redis.googleapis.com/server/uptime",
|
||||
"redis.googleapis.com/stats/memory/usage_ratio",
|
||||
"redis.googleapis.com/stats/memory/system_memory_usage_ratio",
|
||||
"redis.googleapis.com/stats/cpu_utilization_main_thread",
|
||||
"redis.googleapis.com/clients/connected",
|
||||
"redis.googleapis.com/stats/cache_hit_ratio",
|
||||
"redis.googleapis.com/keyspace/keys",
|
||||
"redis.googleapis.com/commands/usec_per_call",
|
||||
"redis.googleapis.com/commands/calls",
|
||||
"redis.googleapis.com/clients/blocked",
|
||||
"redis.googleapis.com/stats/reject_connections_count"
|
||||
]
|
||||
},
|
||||
"logs": {
|
||||
"sinkFilter": "resource.type=\"redis_instance\" OR (protoPayload.serviceName=\"redis.googleapis.com\" AND protoPayload.methodName:\".CloudRedis.\") OR (log_id(\"maintenance.googleapis.com/activity_log\") AND labels.activity_type_name:\"CloudRedisRollout\")"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
|
||||
@@ -263,6 +263,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddFieldTuplesFactory(sqlstore),
|
||||
sqlmigration.NewKeepRelevantLLMPricingRulesFactory(),
|
||||
sqlmigration.NewBackfillTelegramChannelSpecFactory(),
|
||||
sqlmigration.NewAddMetricsFieldTuplesFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
162
pkg/sqlmigration/137_add_metrics_field_tuples.go
Normal file
162
pkg/sqlmigration/137_add_metrics_field_tuples.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addMetricsFieldTuples struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewAddMetricsFieldTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_metrics_field_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addMetricsFieldTuples{sqlstore: sqlstore}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addMetricsFieldTuples) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addMetricsFieldTuples) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var storeID string
|
||||
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var orgIDs []string
|
||||
err = tx.NewSelect().
|
||||
Table("organizations").
|
||||
Column("id").
|
||||
Scan(ctx, &orgIDs)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
|
||||
|
||||
tuples := []migrationTuple{
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "metrics-field", "read"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "metrics-field", "update"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "metrics-field", "list"},
|
||||
{authtypes.SigNozEditorRoleName, "metaresource", "metrics-field", "read"},
|
||||
{authtypes.SigNozEditorRoleName, "metaresource", "metrics-field", "update"},
|
||||
{authtypes.SigNozEditorRoleName, "metaresource", "metrics-field", "list"},
|
||||
{authtypes.SigNozViewerRoleName, "metaresource", "metrics-field", "read"},
|
||||
{authtypes.SigNozViewerRoleName, "metaresource", "metrics-field", "list"},
|
||||
}
|
||||
|
||||
for _, orgID := range orgIDs {
|
||||
for _, tuple := range tuples {
|
||||
entropy := ulid.DefaultEntropy()
|
||||
now := time.Now().UTC()
|
||||
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
|
||||
|
||||
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
|
||||
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
|
||||
|
||||
if isPG {
|
||||
user := "role:" + roleSubject + "#assignee"
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, user, 0, tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
managedRoleGroups := make(map[string]string, len(coretypes.ManagedRoleToTransactions))
|
||||
for roleName, transactions := range coretypes.ManagedRoleToTransactions {
|
||||
data, err := json.Marshal(authtypes.NewTransactionGroupsFromTransactions(transactions))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
managedRoleGroups[roleName] = string(data)
|
||||
}
|
||||
|
||||
for _, orgID := range orgIDs {
|
||||
for roleName, data := range managedRoleGroups {
|
||||
if _, err := tx.NewUpdate().
|
||||
Model(new(roles)).
|
||||
Set("transaction_groups = ?", data).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("type = ?", authtypes.RoleTypeManaged.StringValue()).
|
||||
Where("name = ?", roleName).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addMetricsFieldTuples) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -24,7 +24,19 @@ type GCPConnectionArtifact struct{}
|
||||
|
||||
type GCPIntegrationConfig struct{}
|
||||
|
||||
type GCPTelemetryCollectionStrategy struct{}
|
||||
type GCPTelemetryCollectionStrategy struct {
|
||||
Metrics *GCPMetricsCollectionStrategy `json:"metrics" required:"false" nullable:"false"`
|
||||
Logs *GCPLogsCollectionStrategy `json:"logs" required:"false" nullable:"false"`
|
||||
}
|
||||
|
||||
type GCPMetricsCollectionStrategy struct {
|
||||
MetricNames []string `json:"metricNames" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type GCPLogsCollectionStrategy struct {
|
||||
// Cloud Logging filter for the log sink, https://cloud.google.com/logging/docs/view/logging-query-language
|
||||
SinkFilter string `json:"sinkFilter" required:"true"`
|
||||
}
|
||||
|
||||
type GCPServiceConfig struct {
|
||||
Logs *GCPServiceLogsConfig `json:"logs,omitempty" required:"false"`
|
||||
|
||||
@@ -79,6 +79,7 @@ func (name Kind) Enum() []any {
|
||||
KindMeterMetrics,
|
||||
KindLogsField,
|
||||
KindTracesField,
|
||||
KindMetricsField,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ var Kinds = []Kind{
|
||||
KindMeterMetrics,
|
||||
KindLogsField,
|
||||
KindTracesField,
|
||||
KindMetricsField,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -80,4 +81,5 @@ var (
|
||||
KindMeterMetrics = MustNewKind("meter-metrics")
|
||||
KindLogsField = MustNewKind("logs-field")
|
||||
KindTracesField = MustNewKind("traces-field")
|
||||
KindMetricsField = MustNewKind("metrics-field")
|
||||
)
|
||||
|
||||
@@ -214,6 +214,10 @@ var ManagedRoleToTransactions = map[string][]Transaction{
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
|
||||
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
|
||||
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
|
||||
// metrics-field — editor+admin update, viewer reads
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindMetricsField}, WildCardSelectorString)},
|
||||
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindMetricsField}, WildCardSelectorString)},
|
||||
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindMetricsField}, WildCardSelectorString)},
|
||||
},
|
||||
SigNozEditorRoleName: {
|
||||
// cloud-integration — editor can read accounts
|
||||
@@ -309,6 +313,10 @@ var ManagedRoleToTransactions = map[string][]Transaction{
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
|
||||
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
|
||||
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
|
||||
// metrics-field — editor reads+updates
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindMetricsField}, WildCardSelectorString)},
|
||||
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindMetricsField}, WildCardSelectorString)},
|
||||
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindMetricsField}, WildCardSelectorString)},
|
||||
},
|
||||
SigNozViewerRoleName: {
|
||||
// cloud-integration — viewer can read accounts
|
||||
@@ -370,6 +378,9 @@ var ManagedRoleToTransactions = map[string][]Transaction{
|
||||
// traces-field — viewer reads
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
|
||||
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
|
||||
// metrics-field — viewer reads
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindMetricsField}, WildCardSelectorString)},
|
||||
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindMetricsField}, WildCardSelectorString)},
|
||||
},
|
||||
SigNozAnonymousRoleName: {
|
||||
// public-dashboard — anonymous read
|
||||
|
||||
@@ -39,6 +39,7 @@ var Resources = []Resource{
|
||||
ResourceTelemetryResourceMeterMetrics,
|
||||
ResourceMetaResourceLogsField,
|
||||
ResourceMetaResourceTracesField,
|
||||
ResourceMetaResourceMetricsField,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -80,4 +81,5 @@ var (
|
||||
ResourceTelemetryResourceMeterMetrics = NewResourceTelemetryResource(KindMeterMetrics)
|
||||
ResourceMetaResourceLogsField = NewResourceMetaResource(KindLogsField)
|
||||
ResourceMetaResourceTracesField = NewResourceMetaResource(KindTracesField)
|
||||
ResourceMetaResourceMetricsField = NewResourceMetaResource(KindMetricsField, VerbList, VerbRead, VerbUpdate)
|
||||
)
|
||||
|
||||
13
tests/fixtures/cloudintegrations.py
vendored
13
tests/fixtures/cloudintegrations.py
vendored
@@ -49,9 +49,6 @@ class ProviderAccountSpec:
|
||||
class ProviderServiceSpec:
|
||||
provider: str
|
||||
service_id: str
|
||||
# GCP ships every service with supportedSignals.logs false, so a logs block
|
||||
# is neither required on write nor persisted.
|
||||
supports_logs: bool
|
||||
account_config: dict
|
||||
# id shown in parametrized test names; defaults to the provider slug.
|
||||
id: str = field(default="")
|
||||
@@ -61,10 +58,12 @@ class ProviderServiceSpec:
|
||||
object.__setattr__(self, "id", self.provider)
|
||||
|
||||
def build_service_config(self, metrics_enabled: bool, logs_enabled: bool | None = None) -> dict:
|
||||
config: dict = {"metrics": {"enabled": metrics_enabled}}
|
||||
if self.supports_logs:
|
||||
config["logs"] = {"enabled": metrics_enabled if logs_enabled is None else logs_enabled}
|
||||
return {self.provider: config}
|
||||
return {
|
||||
self.provider: {
|
||||
"metrics": {"enabled": metrics_enabled},
|
||||
"logs": {"enabled": metrics_enabled if logs_enabled is None else logs_enabled},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
|
||||
@@ -288,6 +288,15 @@
|
||||
"list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "metaresource",
|
||||
"kind": "metrics-field",
|
||||
"verbs": [
|
||||
"read",
|
||||
"update",
|
||||
"list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "metaresource",
|
||||
"kind": "ttl-setting",
|
||||
@@ -510,6 +519,15 @@
|
||||
"list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "metaresource",
|
||||
"kind": "metrics-field",
|
||||
"verbs": [
|
||||
"read",
|
||||
"update",
|
||||
"list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "metaresource",
|
||||
"kind": "ttl-setting",
|
||||
@@ -681,6 +699,14 @@
|
||||
"list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "metaresource",
|
||||
"kind": "metrics-field",
|
||||
"verbs": [
|
||||
"read",
|
||||
"list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "metaresource",
|
||||
"kind": "ttl-setting",
|
||||
|
||||
@@ -19,14 +19,12 @@ logger = setup_logger(__name__)
|
||||
AWS_SERVICE_SPEC = ProviderServiceSpec(
|
||||
provider="aws",
|
||||
service_id="rds",
|
||||
supports_logs=True,
|
||||
account_config={"aws": {"deploymentRegion": "us-east-1", "regions": ["us-east-1"]}},
|
||||
)
|
||||
|
||||
GCP_SERVICE_SPEC = ProviderServiceSpec(
|
||||
provider="gcp",
|
||||
service_id="cloudsql_postgres",
|
||||
supports_logs=False,
|
||||
account_config={
|
||||
"gcp": {
|
||||
"deploymentProjectId": "signoz-test-project",
|
||||
@@ -162,7 +160,7 @@ def test_get_service_details_without_account(
|
||||
assert data["cloudIntegrationService"] is None, "cloudIntegrationService should be null without account context"
|
||||
|
||||
assert data["supportedSignals"]["metrics"] is True, "metrics should be a supported signal"
|
||||
assert data["supportedSignals"]["logs"] is spec.supports_logs, f"logs support should be {spec.supports_logs} for {spec.provider}"
|
||||
assert data["supportedSignals"]["logs"] is True, "logs should be a supported signal"
|
||||
|
||||
|
||||
@provider_spec
|
||||
@@ -253,10 +251,7 @@ def test_update_service_config(
|
||||
assert svc["config"][spec.provider]["metrics"]["enabled"] is True, "metrics should be enabled"
|
||||
assert svc["cloudIntegrationId"] == account_id, "cloudIntegrationId should match the account"
|
||||
|
||||
if spec.supports_logs:
|
||||
assert svc["config"][spec.provider]["logs"]["enabled"] is True, "logs should be enabled"
|
||||
else:
|
||||
assert svc["config"][spec.provider].get("logs") is None, f"logs should not be stored for {spec.provider}, got: {svc['config'][spec.provider]}"
|
||||
assert svc["config"][spec.provider]["logs"]["enabled"] is True, "logs should be enabled"
|
||||
|
||||
|
||||
@provider_spec
|
||||
@@ -307,8 +302,7 @@ def test_update_service_config_disable(
|
||||
assert svc is not None, "cloudIntegrationService should still be present after disable"
|
||||
assert svc["config"][spec.provider]["metrics"]["enabled"] is False, "metrics should be disabled"
|
||||
|
||||
if spec.supports_logs:
|
||||
assert svc["config"][spec.provider]["logs"]["enabled"] is False, "logs should be disabled"
|
||||
assert svc["config"][spec.provider]["logs"]["enabled"] is False, "logs should be disabled"
|
||||
|
||||
|
||||
@provider_spec
|
||||
@@ -337,7 +331,7 @@ def test_update_gcp_service_without_metrics_config(
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
) -> None:
|
||||
"""GCP services support metrics only, so a config omitting metrics is rejected."""
|
||||
"""A GCP config omitting metrics is rejected."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
account = create_cloud_integration_account(admin_token, "gcp", config=GCP_SERVICE_SPEC.account_config)
|
||||
|
||||
286
tests/integration/tests/metricsexplorer/01_authz.py
Normal file
286
tests/integration/tests/metricsexplorer/01_authz.py
Normal file
@@ -0,0 +1,286 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from wiremock.resources.mappings import Mapping
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import (
|
||||
USER_ADMIN_EMAIL,
|
||||
USER_ADMIN_PASSWORD,
|
||||
add_license,
|
||||
change_user_role,
|
||||
create_active_user,
|
||||
)
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.role import transaction_group
|
||||
|
||||
user_password = "password123Z$"
|
||||
scoped_role = "metrics-explorer-scope-key-a"
|
||||
scoped_email = "metrics-explorer-scope-key-a@telemetry.test"
|
||||
wildcard_role = "metrics-explorer-wildcard"
|
||||
wildcard_email = "metrics-explorer-wildcard@telemetry.test"
|
||||
metadata_role = "metrics-explorer-metadata-reader"
|
||||
metadata_email = "metrics-explorer-metadata-reader@telemetry.test"
|
||||
viewer_email = "metrics-explorer-viewer@telemetry.test"
|
||||
editor_email = "metrics-explorer-editor@telemetry.test"
|
||||
|
||||
|
||||
def test_setup(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list[Mapping]], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
create_role: Callable[..., str],
|
||||
) -> None:
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
create_role(admin_token, scoped_role, [transaction_group("read", "telemetryresource", "metrics", ["builder_query/signoz.workspace.key.id/key-a"])])
|
||||
scoped_user = create_active_user(signoz, admin_token, email=scoped_email, role="signoz-viewer", password=user_password)
|
||||
change_user_role(signoz, admin_token, scoped_user, "signoz-viewer", scoped_role)
|
||||
|
||||
create_role(admin_token, wildcard_role, [transaction_group("read", "telemetryresource", "metrics", ["builder_query/*"])])
|
||||
wildcard_user = create_active_user(signoz, admin_token, email=wildcard_email, role="signoz-viewer", password=user_password)
|
||||
change_user_role(signoz, admin_token, wildcard_user, "signoz-viewer", wildcard_role)
|
||||
|
||||
create_role(
|
||||
admin_token,
|
||||
metadata_role,
|
||||
[transaction_group("read", "metaresource", "metrics-field", ["*"]), transaction_group("list", "metaresource", "metrics-field", ["*"])],
|
||||
)
|
||||
metadata_user = create_active_user(signoz, admin_token, email=metadata_email, role="signoz-viewer", password=user_password)
|
||||
change_user_role(signoz, admin_token, metadata_user, "signoz-viewer", metadata_role)
|
||||
|
||||
create_active_user(signoz, admin_token, email=viewer_email, role="signoz-viewer", password=user_password)
|
||||
create_active_user(signoz, admin_token, email=editor_email, role="signoz-editor", password=user_password)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method", "path", "query", "body"),
|
||||
[
|
||||
("POST", "/api/v2/metrics/stats", None, {"limit": 10}),
|
||||
("POST", "/api/v2/metrics/treemap", None, {"limit": 10, "mode": "samples"}),
|
||||
("GET", "/api/v2/metrics/highlights", {"metricName": "explorer.requests"}, None),
|
||||
("POST", "/api/v2/metrics/inspect", None, {"metricName": "explorer.requests"}),
|
||||
("GET", "/api/v2/metrics/onboarding", {}, None),
|
||||
],
|
||||
ids=["stats", "treemap", "highlights", "inspect", "onboarding"],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("email", "expected"),
|
||||
[(scoped_email, HTTPStatus.FORBIDDEN), (metadata_email, HTTPStatus.FORBIDDEN), (wildcard_email, HTTPStatus.OK), (USER_ADMIN_EMAIL, HTTPStatus.OK)],
|
||||
ids=["scoped", "metadata_reader", "wildcard", "admin"],
|
||||
)
|
||||
def test_telemetry_routes_without_filter(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
method: str,
|
||||
path: str,
|
||||
query: dict | None,
|
||||
body: dict | None,
|
||||
email: str,
|
||||
expected: HTTPStatus,
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics([Metrics(metric_name="explorer.requests", labels={"signoz.workspace.key.id": "key-a"}, timestamp=now - timedelta(minutes=i + 1), value=float(i)) for i in range(3)])
|
||||
start, end = int((now - timedelta(minutes=10)).timestamp() * 1000), int(now.timestamp() * 1000)
|
||||
token = get_token(email, USER_ADMIN_PASSWORD if email == USER_ADMIN_EMAIL else user_password)
|
||||
|
||||
response = requests.request(
|
||||
method,
|
||||
signoz.self.host_configs["8080"].get(path),
|
||||
params={"start": start, "end": end, **query} if query is not None else None,
|
||||
json={"start": start, "end": end, **body} if body is not None else None,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
assert response.status_code == expected, response.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("path", "query"),
|
||||
[
|
||||
("/api/v2/metrics", {"limit": 10}),
|
||||
("/api/v2/metrics/attributes", {"metricName": "explorer.requests"}),
|
||||
("/api/v2/metrics/metadata", {"metricName": "explorer.requests"}),
|
||||
],
|
||||
ids=["list", "attributes", "metadata"],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("email", "expected"),
|
||||
[
|
||||
(scoped_email, HTTPStatus.FORBIDDEN),
|
||||
(wildcard_email, HTTPStatus.FORBIDDEN),
|
||||
(metadata_email, HTTPStatus.OK),
|
||||
(viewer_email, HTTPStatus.OK),
|
||||
(USER_ADMIN_EMAIL, HTTPStatus.OK),
|
||||
],
|
||||
ids=["metrics_scoped", "metrics_wildcard", "metadata_reader", "viewer", "admin"],
|
||||
)
|
||||
def test_metadata_reads_need_metadata_grant(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
path: str,
|
||||
query: dict,
|
||||
email: str,
|
||||
expected: HTTPStatus,
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics([Metrics(metric_name="explorer.requests", labels={"signoz.workspace.key.id": "key-a"}, timestamp=now - timedelta(minutes=i + 1), value=float(i)) for i in range(3)])
|
||||
token = get_token(email, USER_ADMIN_PASSWORD if email == USER_ADMIN_EMAIL else user_password)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(path),
|
||||
params={"start": int((now - timedelta(minutes=10)).timestamp() * 1000), "end": int(now.timestamp() * 1000), **query},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
assert response.status_code == expected, response.text
|
||||
if expected == HTTPStatus.FORBIDDEN:
|
||||
assert "metrics-field" in response.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("email", "expected"),
|
||||
[
|
||||
(scoped_email, HTTPStatus.FORBIDDEN),
|
||||
(metadata_email, HTTPStatus.FORBIDDEN),
|
||||
(viewer_email, HTTPStatus.FORBIDDEN),
|
||||
(editor_email, HTTPStatus.OK),
|
||||
(USER_ADMIN_EMAIL, HTTPStatus.OK),
|
||||
],
|
||||
ids=["metrics_scoped", "metadata_reader", "viewer", "editor", "admin"],
|
||||
)
|
||||
def test_metadata_update_needs_update_grant(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
email: str,
|
||||
expected: HTTPStatus,
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics([Metrics(metric_name="explorer.requests", labels={"signoz.workspace.key.id": "key-a"}, timestamp=now - timedelta(minutes=i + 1), value=float(i)) for i in range(3)])
|
||||
token = get_token(email, USER_ADMIN_PASSWORD if email == USER_ADMIN_EMAIL else user_password)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/metrics/metadata"),
|
||||
json={"metricName": "explorer.requests", "type": "sum", "description": "request count", "unit": "1", "temporality": "cumulative", "isMonotonic": True},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
assert response.status_code == expected, response.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("email", "expression", "expected", "expected_metrics"),
|
||||
[
|
||||
(scoped_email, "signoz.workspace.key.id = 'key-a'", HTTPStatus.OK, {"explorer.key.a"}),
|
||||
(scoped_email, "signoz.workspace.key.id = 'key-b'", HTTPStatus.FORBIDDEN, set()),
|
||||
(scoped_email, "signoz.workspace.key.id IN ('key-a', 'key-b')", HTTPStatus.FORBIDDEN, set()),
|
||||
(wildcard_email, None, HTTPStatus.OK, {"explorer.key.a", "explorer.key.b"}),
|
||||
],
|
||||
ids=["scoped_granted_key", "scoped_other_key", "scoped_both_keys", "wildcard_unfiltered"],
|
||||
)
|
||||
def test_stats_scoped_read(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
email: str,
|
||||
expression: str | None,
|
||||
expected: HTTPStatus,
|
||||
expected_metrics: set[str],
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics(
|
||||
[Metrics(metric_name="explorer.key.a", labels={"signoz.workspace.key.id": "key-a"}, timestamp=now - timedelta(minutes=i + 1), value=float(i)) for i in range(3)]
|
||||
+ [Metrics(metric_name="explorer.key.b", labels={"signoz.workspace.key.id": "key-b"}, timestamp=now - timedelta(minutes=i + 1), value=float(i)) for i in range(3)]
|
||||
)
|
||||
|
||||
body: dict = {"start": int((now - timedelta(minutes=10)).timestamp() * 1000), "end": int(now.timestamp() * 1000), "limit": 50}
|
||||
if expression:
|
||||
body["filter"] = {"expression": expression}
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/metrics/stats"),
|
||||
json=body,
|
||||
headers={"Authorization": f"Bearer {get_token(email, user_password)}"},
|
||||
timeout=10,
|
||||
)
|
||||
assert response.status_code == expected, response.text
|
||||
if expected == HTTPStatus.OK:
|
||||
assert {stat["metricName"] for stat in response.json()["data"]["metrics"]} == expected_metrics
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("path", "body"),
|
||||
[
|
||||
("/api/v2/metrics/treemap", {"limit": 10, "mode": "samples"}),
|
||||
("/api/v2/metrics/inspect", {"metricName": "explorer.key.a"}),
|
||||
],
|
||||
ids=["treemap", "inspect"],
|
||||
)
|
||||
def test_filtered_routes_allow_granted_key(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
path: str,
|
||||
body: dict,
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics([Metrics(metric_name="explorer.key.a", labels={"signoz.workspace.key.id": "key-a"}, timestamp=now - timedelta(minutes=i + 1), value=float(i)) for i in range(3)])
|
||||
start, end = int((now - timedelta(minutes=10)).timestamp() * 1000), int(now.timestamp() * 1000)
|
||||
token = get_token(scoped_email, user_password)
|
||||
|
||||
allowed = requests.post(
|
||||
signoz.self.host_configs["8080"].get(path),
|
||||
json={"start": start, "end": end, "filter": {"expression": "signoz.workspace.key.id = 'key-a'"}, **body},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
assert allowed.status_code == HTTPStatus.OK, allowed.text
|
||||
|
||||
denied = requests.post(
|
||||
signoz.self.host_configs["8080"].get(path),
|
||||
json={"start": start, "end": end, "filter": {"expression": "signoz.workspace.key.id = 'key-b'"}, **body},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
assert denied.status_code == HTTPStatus.FORBIDDEN, denied.text
|
||||
assert "builder_query/signoz.workspace.key.id/key-b" in denied.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("path", "scope"),
|
||||
[("/api/v2/metrics/alerts", "rule:list"), ("/api/v3/metrics/dashboards", "dashboard:list")],
|
||||
ids=["alerts", "dashboards"],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("email", "expected"),
|
||||
[(scoped_email, HTTPStatus.FORBIDDEN), (wildcard_email, HTTPStatus.FORBIDDEN), (USER_ADMIN_EMAIL, HTTPStatus.OK)],
|
||||
ids=["metrics_scoped", "metrics_wildcard", "admin"],
|
||||
)
|
||||
def test_metaresource_lookups_need_list_grant(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
path: str,
|
||||
scope: str,
|
||||
email: str,
|
||||
expected: HTTPStatus,
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics([Metrics(metric_name="explorer.requests", labels={"signoz.workspace.key.id": "key-a"}, timestamp=now - timedelta(minutes=i + 1), value=float(i)) for i in range(3)])
|
||||
token = get_token(email, USER_ADMIN_PASSWORD if email == USER_ADMIN_EMAIL else user_password)
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(path),
|
||||
params={"metricName": "explorer.requests"},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
assert response.status_code == expected, response.text
|
||||
if expected == HTTPStatus.FORBIDDEN:
|
||||
assert scope.split(":", maxsplit=1)[0] in response.text
|
||||
Reference in New Issue
Block a user