Compare commits

..

25 Commits

Author SHA1 Message Date
nityanandagohain
0a59a9ef2d Merge remote-tracking branch 'origin/main' into issue_5601_spanlist 2026-08-07 16:57:54 +05:30
nityanandagohain
f0bac9f6be fix: more changes 2026-07-24 16:47:49 +05:30
nityanandagohain
2d22cb6374 fix: rename file 2026-07-24 16:18:57 +05:30
nityanandagohain
5f3d869058 Merge branch 'issue_5601' into issue_5601_spanlist 2026-07-24 16:11:14 +05:30
nityanandagohain
b2c5af428e fix: refactor as requested 2026-07-24 12:07:37 +05:30
nityanandagohain
093f9b41c4 fix: minor cleanup 2026-07-22 16:22:19 +05:30
nityanandagohain
596e128005 fix: updated openapi 2026-07-22 15:26:52 +05:30
nityanandagohain
2e1da92367 fix: remove source and change to builder ai query 2026-07-22 15:24:43 +05:30
nityanandagohain
1e8d72e8fa Merge remote-tracking branch 'origin/main' into issue_5601 2026-07-21 17:08:34 +05:30
nityanandagohain
cce080e1ae fix: add back the flag in metadata 2026-07-16 11:37:57 +05:30
nityanandagohain
deca060a4d Merge remote-tracking branch 'origin/main' into issue_5601 2026-07-16 11:00:55 +05:30
nityanandagohain
03c7e524e7 fix: fix tests 2026-07-15 20:23:33 +05:30
nityanandagohain
815dc7d88b Merge remote-tracking branch 'origin/main' into issue_5601 2026-07-15 20:10:05 +05:30
nityanandagohain
f50d9199fe fix: address comments 2026-07-15 19:47:39 +05:30
nityanandagohain
97c49c870b feat: support ai trace aggregate filtering in ai span list 2026-07-15 12:27:55 +05:30
nityanandagohain
31efe177a4 fix: address comments 2026-07-14 18:53:30 +05:30
nityanandagohain
d502d12ac3 fix: update openapi 2026-07-10 14:27:07 +05:30
nityanandagohain
bd9f15a716 fix: update integration test 2026-07-10 14:21:16 +05:30
nityanandagohain
813ef988c9 fix: edge cases and correct cost key 2026-07-10 12:06:34 +05:30
nityanandagohain
40e6799285 fix: add resource fingerprint cte 2026-07-10 00:36:25 +05:30
nityanandagohain
1caa60a3cd fix: cleanup and more tests 2026-07-09 23:54:05 +05:30
nityanandagohain
3f781f0083 fix: more cleanup 2026-07-09 12:39:01 +05:30
nityanandagohain
6aec05cf7a fix: more tests 2026-07-09 08:45:22 +05:30
nityanandagohain
683a52f35a fix: take perf into consideration 2026-07-09 08:45:22 +05:30
nityanandagohain
e924fa1e62 feat: support llm trace list and span list 2026-07-09 08:45:20 +05:30
210 changed files with 4620 additions and 13924 deletions

View File

@@ -1,11 +0,0 @@
# Comments
Applies to everything in the repo — code, config, workflows.
- **No unnecessary comments.** Do not comment where the code is self-explanatory; never restate what the code already says.
- **Document only** non-obvious behavior, constraints, formats, and edge cases.
- **Rationale goes in prose, not source.** Why a version is pinned, why a job exists, how a subsystem fits together — that belongs in the README or the PR.
- **Never remove pre-existing comments** when editing code. The bar above applies to comments you write, not comments already there.
- **Never talk to the reviewer.** No comments about where a change came from, what was changed, or why the change is correct — that belongs in the PR description and is noise the moment it merges.
Language rules build on this one: [`go-comments`](go-comments.md), [`py-comments`](py-comments.md).

View File

@@ -1,12 +0,0 @@
---
paths:
- "**/*.go"
---
# Go comments
The bar is the [`comments`](comments.md) rule: nothing where the code is self-explanatory.
- **Names carry the meaning.** Make function, type, and variable names self-explanatory so the comment is unnecessary in the first place. If a comment is needed to explain what a function does, fix the name, not the comment.
- **Godoc**: Skip comments that merely restate the identifier. Document only non-obvious behavior, constraints, formats, and edge cases.
- **Generated code**: If the comment is emitted by an external codegen tool, leave it as-is — do not add or trim comments in generated files.

View File

@@ -1,7 +0,0 @@
# Pull requests
- **Follow the template** (`.github/pull_request_template.md`): fill in its headings (Description / Issues closed by this PR / Screenshots / Additional Information). Don't add sections the template doesn't have.
- **Keep only the headings that apply.** Delete every heading that has nothing under it, along with its `<!--...-->` placeholder comment. The body must never contain an empty heading — if only Description applies, the body has exactly that one heading.
- **Keep the description concise and human-readable.** A few plain bullets saying what changed and why, for a reviewer skimming it — not a wall of text, not a restatement of the diff, not generated boilerplate.
- **Reference issues with `Closes #issue-number`** under "Issues closed by this PR" so they auto-close on merge. This goes in the PR description only — never in commit messages.
- **AI assistance in commits may optionally be disclosed with an `Assisted-by:` trailer** naming the model (e.g. `Assisted-by: Claude Opus 4.5`) — do NOT use a `Co-authored-by:` trailer for this.

View File

@@ -1,13 +0,0 @@
---
paths:
- "**/*.py"
---
# Python comments
The bar is the [`comments`](comments.md) rule: nothing where the code is self-explanatory.
- **Names carry the meaning.** Make function and variable names self-explanatory so the comment or docstring is unnecessary in the first place. If a docstring is needed to explain what a function does, fix the name, not the docstring.
- **No file-level docstring.** The filename says what the module is for — `tool_bin.py` gets the tool binary. A module docstring restating that is noise, and a paragraph of design prose at the top of a file goes stale where nobody is looking. A constraint belongs next to the code it constrains, not in a preamble.
- **Docstrings**: only when they say something the name and signature don't — drop them otherwise. Keep them short. A contract that genuinely needs a few lines (interacting flags, retry semantics, an edge case) is fine; a narrative is not.
- **No song and dance.** Comment the constraint or the edge case. Not the narrative, not the rationale, not what the next line does.

View File

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

View File

@@ -1,13 +1,85 @@
<!--A few plain bullets saying what changed and why, for a reviewer skimming it - not a wall of text, not a restatement of the diff, not generated boilerplate.-->
#### Description
## Pull Request
---
### 📄 Summary
> Why does this change exist?
> What problem does it solve, and why is this the right approach?
#### Screenshots / Screen Recordings (if applicable)
> Include screenshots or screen recordings that clearly show the behavior before the change and the result after the change. This helps reviewers quickly understand the impact and verify the update.
<!--Reference issues using `Closes #issue-number` to enable automatic closure on merge. -->
#### Issues closed by this PR
> Reference issues using `Closes #issue-number` to enable automatic closure on merge.
<!--If applicable, include screenshots or screen recordings that clearly show the behavior before the change and the result after the change. -->
#### Screenshots / Screen Recordings
---
<!--Anything reviewers should keep in mind while reviewing -->
#### Additional Information
### ✅ Change Type
_Select all that apply_
<!--Please delete paragraphs that you did not use before submitting.-->
- [ ] ✨ Feature
- [ ] 🐛 Bug fix
- [ ] ♻️ Refactor
- [ ] 🛠️ Infra / Tooling
- [ ] 🧪 Test-only
---
### 🐛 Bug Context
> Required if this PR fixes a bug
#### Root Cause
> What caused the issue?
> Regression, faulty assumption, edge case, refactor, etc.
#### Fix Strategy
> How does this PR address the root cause?
---
### 🧪 Testing Strategy
> How was this change validated?
- Tests added/updated:
- Manual verification:
- Edge cases covered:
---
### ⚠️ Risk & Impact Assessment
> What could break? How do we recover?
- Blast radius:
- Potential regressions:
- Rollback plan:
---
### 📝 Changelog
> Fill only if this affects users, APIs, UI, or documented behavior
> Use **N/A** for internal or non-user-facing changes
| Field | Value |
|------|-------|
| Deployment Type | Cloud / OSS / Enterprise |
| Change Type | Feature / Bug Fix / Maintenance |
| Description | User-facing summary |
---
### 📋 Checklist
- [ ] Tests added or explicitly not required
- [ ] Manually tested
- [ ] Breaking changes documented
- [ ] Backward compatibility considered
---
## 👀 Notes for Reviewers
<!-- Anything reviewers should keep in mind while reviewing -->
---

83
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,83 @@
name: "Update PR labels and Block PR until related docs are shipped for the feature"
on:
pull_request:
branches:
- main
types: [opened, edited, labeled, unlabeled]
permissions:
pull-requests: write
contents: read
jobs:
docs_label_check:
runs-on: ubuntu-latest
steps:
- name: Check PR Title and Manage Labels
uses: actions/github-script@v6
with:
script: |
const prTitle = context.payload.pull_request.title;
const prNumber = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
// Fetch the current PR details to get labels
const pr = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber
});
const labels = pr.data.labels.map(label => label.name);
if (prTitle.startsWith('feat:')) {
const hasDocsRequired = labels.includes('docs required');
const hasDocsShipped = labels.includes('docs shipped');
const hasDocsNotRequired = labels.includes('docs not required');
// If "docs not required" is present, skip the checks
if (hasDocsNotRequired && !hasDocsRequired) {
console.log("Skipping checks due to 'docs not required' label.");
return; // Exit the script early
}
// If "docs shipped" is present, remove "docs required" if it exists
if (hasDocsShipped && hasDocsRequired) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: 'docs required'
});
console.log("Removed 'docs required' label.");
}
// Add "docs required" label if neither "docs shipped" nor "docs required" are present
if (!hasDocsRequired && !hasDocsShipped) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: ['docs required']
});
console.log("Added 'docs required' label.");
}
}
// Fetch the updated labels after any changes
const updatedPr = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber
});
const updatedLabels = updatedPr.data.labels.map(label => label.name);
const updatedHasDocsRequired = updatedLabels.includes('docs required');
const updatedHasDocsShipped = updatedLabels.includes('docs shipped');
// Block PR if "docs required" is still present and "docs shipped" is missing
if (updatedHasDocsRequired && !updatedHasDocsShipped) {
core.setFailed("This PR requires documentation. Please remove the 'docs required' label and add the 'docs shipped' label to proceed.");
}

View File

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

6
.gitignore vendored
View File

@@ -90,6 +90,8 @@ queries.active
.devenv/**/tmp/**
.qodo
.dev
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -229,6 +231,4 @@ cython_debug/
# LSP config files
pyrightconfig.json
# dev
.dev/
.claude/worktrees/

View File

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

View File

@@ -4212,21 +4212,6 @@ components:
- missingOptionalMetrics
- missingRequiredAttributes
type: object
InframonitoringtypesClusterFilter:
properties:
expression:
type: string
filterByNodeReadiness:
items:
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
nullable: true
type: array
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesClusterRecord:
properties:
clusterCPU:
@@ -4364,16 +4349,6 @@ components:
- containerCannotRun
- unknown
type: object
InframonitoringtypesContainerFilter:
properties:
expression:
type: string
filterByContainerStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesContainerStatus'
nullable: true
type: array
type: object
InframonitoringtypesContainerReady:
enum:
- ready
@@ -4473,16 +4448,6 @@ components:
- total
- endTimeBeforeRetention
type: object
InframonitoringtypesDaemonSetFilter:
properties:
expression:
type: string
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesDaemonSetRecord:
properties:
currentNodes:
@@ -4555,16 +4520,6 @@ components:
- total
- endTimeBeforeRetention
type: object
InframonitoringtypesDeploymentFilter:
properties:
expression:
type: string
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesDeploymentRecord:
properties:
availablePods:
@@ -4706,16 +4661,6 @@ components:
- total
- endTimeBeforeRetention
type: object
InframonitoringtypesJobFilter:
properties:
expression:
type: string
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesJobRecord:
properties:
activePods:
@@ -4839,16 +4784,6 @@ components:
- message
- documentationLink
type: object
InframonitoringtypesNamespaceFilter:
properties:
expression:
type: string
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesNamespaceRecord:
properties:
counts:
@@ -4930,21 +4865,6 @@ components:
- ready
- notReady
type: object
InframonitoringtypesNodeFilter:
properties:
expression:
type: string
filterByNodeReadiness:
items:
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
nullable: true
type: array
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesNodeRecord:
properties:
condition:
@@ -5061,16 +4981,6 @@ components:
- shutdown
- unexpectedAdmissionError
type: object
InframonitoringtypesPodFilter:
properties:
expression:
type: string
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesPodRecord:
properties:
meta:
@@ -5170,7 +5080,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesClusterFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5196,7 +5106,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesContainerFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5222,7 +5132,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesDaemonSetFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5248,7 +5158,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesDeploymentFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5300,7 +5210,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesJobFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5326,7 +5236,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesNamespaceFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5352,7 +5262,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesNodeFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5378,7 +5288,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesPodFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5404,7 +5314,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesStatefulSetFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5455,16 +5365,6 @@ components:
- list
- grouped_list
type: string
InframonitoringtypesStatefulSetFilter:
properties:
expression:
type: string
filterByPodStatus:
items:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
nullable: true
type: array
type: object
InframonitoringtypesStatefulSetRecord:
properties:
currentPods:
@@ -7859,112 +7759,6 @@ components:
enum:
- basic
type: string
SavedviewtypesDisplay:
properties:
color:
type: string
fontSize:
type: string
format:
type: string
maxLines:
type: integer
type: object
SavedviewtypesPanelType:
enum:
- value
- graph
- table
- list
- trace
type: string
SavedviewtypesPostableSavedView:
properties:
data:
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
generateName:
type: boolean
name:
type: string
source:
$ref: '#/components/schemas/SavedviewtypesSource'
required:
- source
- data
type: object
SavedviewtypesSavedView:
properties:
createdAt:
format: date-time
type: string
createdBy:
type: string
data:
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
id:
type: string
name:
type: string
source:
$ref: '#/components/schemas/SavedviewtypesSource'
updatedAt:
format: date-time
type: string
updatedBy:
type: string
required:
- id
type: object
SavedviewtypesSavedViewData:
properties:
schemaVersion:
type: string
spec:
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
required:
- schemaVersion
- spec
type: object
SavedviewtypesSavedViewSpec:
properties:
display:
$ref: '#/components/schemas/SavedviewtypesDisplay'
displayName:
type: string
panelType:
$ref: '#/components/schemas/SavedviewtypesPanelType'
queries:
items:
$ref: '#/components/schemas/Querybuildertypesv5QueryEnvelope'
type: array
selectedFields:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
type: array
required:
- displayName
- panelType
- queries
- selectedFields
- display
type: object
SavedviewtypesSource:
enum:
- traces
- logs
- metrics
- meter
type: string
SavedviewtypesUpdatableSavedView:
properties:
data:
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
source:
$ref: '#/components/schemas/SavedviewtypesSource'
required:
- source
- data
type: object
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
properties:
id:
@@ -8938,6 +8732,15 @@ components:
required:
- id
type: object
TypesPostableBulkInviteRequest:
properties:
invites:
items:
$ref: '#/components/schemas/TypesPostableInvite'
type: array
required:
- invites
type: object
TypesPostableForgotPassword:
properties:
email:
@@ -11545,6 +11348,57 @@ paths:
summary: Create invite
tags:
- users
/api/v1/invite/bulk:
post:
deprecated: true
description: This endpoint creates a bulk invite for a user
operationId: CreateBulkInvite
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableBulkInviteRequest'
responses:
"201":
description: Created
"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
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create bulk invite
tags:
- users
/api/v1/llm_pricing_rules:
get:
deprecated: false
@@ -12198,9 +12052,9 @@ paths:
- dashboard
/api/v1/resetPassword:
post:
deprecated: true
deprecated: false
description: This endpoint resets the password by token
operationId: ResetPasswordDeprecated
operationId: ResetPassword
requestBody:
content:
application/json:
@@ -14468,6 +14322,177 @@ paths:
summary: List users
tags:
- users
/api/v1/user/{id}:
delete:
deprecated: true
description: This endpoint deletes the user by id
operationId: DeleteUserDeprecated
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Delete user
tags:
- users
get:
deprecated: true
description: This endpoint returns the user by id
operationId: GetUserDeprecated
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TypesDeprecatedUser'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Get user
tags:
- users
put:
deprecated: true
description: This endpoint updates the user by id
operationId: UpdateUserDeprecated
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesDeprecatedUser'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TypesDeprecatedUser'
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
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Update user
tags:
- users
/api/v1/user/me:
get:
deprecated: true
@@ -15567,41 +15592,6 @@ paths:
summary: Forgot password
tags:
- users
/api/v2/factor_password/reset:
post:
deprecated: false
description: This endpoint resets the password using a single use reset password
token
operationId: ResetPassword
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableResetPassword'
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
summary: Reset password
tags:
- users
/api/v2/features:
get:
deprecated: false
@@ -22669,298 +22659,6 @@ paths:
summary: Test alert rule
tags:
- rules
/api/v2/saved_views:
get:
deprecated: false
description: Returns saved views, optionally filtered by source and name.
operationId: ListSavedViews
parameters:
- in: query
name: source
schema:
$ref: '#/components/schemas/SavedviewtypesSource'
- in: query
name: name
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/SavedviewtypesSavedView'
nullable: true
type: array
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:
- saved-view:list
- tokenizer:
- saved-view:list
summary: List saved views
tags:
- saved_view
post:
deprecated: false
description: Persists a saved view for the explore page. Returns the id of the
created view.
operationId: CreateSavedView
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SavedviewtypesPostableSavedView'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TypesIdentifiable'
status:
type: string
required:
- status
- data
type: object
description: Created
"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:
- saved-view:create
- tokenizer:
- saved-view:create
summary: Create saved view
tags:
- saved_view
/api/v2/saved_views/{id}:
delete:
deprecated: false
description: Deletes a saved view by id.
operationId: DeleteSavedView
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"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
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- saved-view:delete
- tokenizer:
- saved-view:delete
summary: Delete saved view
tags:
- saved_view
get:
deprecated: false
description: Returns a saved view by id.
operationId: GetSavedView
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/SavedviewtypesSavedView'
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
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- saved-view:read
- tokenizer:
- saved-view:read
summary: Get saved view
tags:
- saved_view
put:
deprecated: false
description: Replaces a saved view's name and query.
operationId: UpdateSavedView
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SavedviewtypesUpdatableSavedView'
responses:
"204":
description: No Content
"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
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- saved-view:update
- tokenizer:
- saved-view:update
summary: Update saved view
tags:
- saved_view
/api/v2/sessions:
delete:
deprecated: false
@@ -23421,12 +23119,6 @@ paths:
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
@@ -23773,12 +23465,6 @@ paths:
responses:
"200":
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
@@ -23831,12 +23517,6 @@ paths:
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
@@ -23924,12 +23604,6 @@ paths:
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:

View File

@@ -1,490 +0,0 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
CreateSavedView201,
DeleteSavedViewPathParameters,
GetSavedView200,
GetSavedViewPathParameters,
ListSavedViews200,
ListSavedViewsParams,
RenderErrorResponseDTO,
SavedviewtypesPostableSavedViewDTO,
SavedviewtypesUpdatableSavedViewDTO,
UpdateSavedViewPathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns saved views, optionally filtered by source and name.
* @summary List saved views
*/
export const listSavedViews = (
params?: ListSavedViewsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListSavedViews200>({
url: `/api/v2/saved_views`,
method: 'GET',
params,
signal,
});
};
export const getListSavedViewsQueryKey = (params?: ListSavedViewsParams) => {
return [`/api/v2/saved_views`, ...(params ? [params] : [])] as const;
};
export const getListSavedViewsQueryOptions = <
TData = Awaited<ReturnType<typeof listSavedViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: ListSavedViewsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listSavedViews>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListSavedViewsQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listSavedViews>>> = ({
signal,
}) => listSavedViews(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listSavedViews>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListSavedViewsQueryResult = NonNullable<
Awaited<ReturnType<typeof listSavedViews>>
>;
export type ListSavedViewsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List saved views
*/
export function useListSavedViews<
TData = Awaited<ReturnType<typeof listSavedViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: ListSavedViewsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listSavedViews>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListSavedViewsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List saved views
*/
export const invalidateListSavedViews = async (
queryClient: QueryClient,
params?: ListSavedViewsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListSavedViewsQueryKey(params) },
options,
);
return queryClient;
};
/**
* Persists a saved view for the explore page. Returns the id of the created view.
* @summary Create saved view
*/
export const createSavedView = (
savedviewtypesPostableSavedViewDTO?: BodyType<SavedviewtypesPostableSavedViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateSavedView201>({
url: `/api/v2/saved_views`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: savedviewtypesPostableSavedViewDTO,
signal,
});
};
export const getCreateSavedViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createSavedView>>,
TError,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createSavedView>>,
TError,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
TContext
> => {
const mutationKey = ['createSavedView'];
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 createSavedView>>,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> }
> = (props) => {
const { data } = props ?? {};
return createSavedView(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateSavedViewMutationResult = NonNullable<
Awaited<ReturnType<typeof createSavedView>>
>;
export type CreateSavedViewMutationBody =
| BodyType<SavedviewtypesPostableSavedViewDTO>
| undefined;
export type CreateSavedViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create saved view
*/
export const useCreateSavedView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createSavedView>>,
TError,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createSavedView>>,
TError,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
TContext
> => {
return useMutation(getCreateSavedViewMutationOptions(options));
};
/**
* Deletes a saved view by id.
* @summary Delete saved view
*/
export const deleteSavedView = (
{ id }: DeleteSavedViewPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/saved_views/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteSavedViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteSavedView>>,
TError,
{ pathParams: DeleteSavedViewPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteSavedView>>,
TError,
{ pathParams: DeleteSavedViewPathParameters },
TContext
> => {
const mutationKey = ['deleteSavedView'];
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 deleteSavedView>>,
{ pathParams: DeleteSavedViewPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteSavedView(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteSavedViewMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteSavedView>>
>;
export type DeleteSavedViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete saved view
*/
export const useDeleteSavedView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteSavedView>>,
TError,
{ pathParams: DeleteSavedViewPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteSavedView>>,
TError,
{ pathParams: DeleteSavedViewPathParameters },
TContext
> => {
return useMutation(getDeleteSavedViewMutationOptions(options));
};
/**
* Returns a saved view by id.
* @summary Get saved view
*/
export const getSavedView = (
{ id }: GetSavedViewPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetSavedView200>({
url: `/api/v2/saved_views/${id}`,
method: 'GET',
signal,
});
};
export const getGetSavedViewQueryKey = ({ id }: GetSavedViewPathParameters) => {
return [`/api/v2/saved_views/${id}`] as const;
};
export const getGetSavedViewQueryOptions = <
TData = Awaited<ReturnType<typeof getSavedView>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetSavedViewPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSavedView>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetSavedViewQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getSavedView>>> = ({
signal,
}) => getSavedView({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getSavedView>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetSavedViewQueryResult = NonNullable<
Awaited<ReturnType<typeof getSavedView>>
>;
export type GetSavedViewQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get saved view
*/
export function useGetSavedView<
TData = Awaited<ReturnType<typeof getSavedView>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetSavedViewPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSavedView>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetSavedViewQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get saved view
*/
export const invalidateGetSavedView = async (
queryClient: QueryClient,
{ id }: GetSavedViewPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetSavedViewQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* Replaces a saved view's name and query.
* @summary Update saved view
*/
export const updateSavedView = (
{ id }: UpdateSavedViewPathParameters,
savedviewtypesUpdatableSavedViewDTO?: BodyType<SavedviewtypesUpdatableSavedViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/saved_views/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: savedviewtypesUpdatableSavedViewDTO,
signal,
});
};
export const getUpdateSavedViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateSavedView>>,
TError,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateSavedView>>,
TError,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
},
TContext
> => {
const mutationKey = ['updateSavedView'];
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 updateSavedView>>,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateSavedView(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateSavedViewMutationResult = NonNullable<
Awaited<ReturnType<typeof updateSavedView>>
>;
export type UpdateSavedViewMutationBody =
| BodyType<SavedviewtypesUpdatableSavedViewDTO>
| undefined;
export type UpdateSavedViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update saved view
*/
export const useUpdateSavedView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateSavedView>>,
TError,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateSavedView>>,
TError,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
},
TContext
> => {
return useMutation(getUpdateSavedViewMutationOptions(options));
};

View File

@@ -5648,47 +5648,6 @@ export interface InframonitoringtypesChecksDTO {
type: InframonitoringtypesCheckTypeDTO;
}
export enum InframonitoringtypesNodeConditionDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export enum InframonitoringtypesPodStatusDTO {
pending = 'pending',
running = 'running',
failed = 'failed',
unknown = 'unknown',
crashloopbackoff = 'crashloopbackoff',
imagepullbackoff = 'imagepullbackoff',
errimagepull = 'errimagepull',
createcontainerconfigerror = 'createcontainerconfigerror',
containercreating = 'containercreating',
oomkilled = 'oomkilled',
completed = 'completed',
error = 'error',
containercannotrun = 'containercannotrun',
evicted = 'evicted',
nodeaffinity = 'nodeaffinity',
nodelost = 'nodelost',
shutdown = 'shutdown',
unexpectedadmissionerror = 'unexpectedadmissionerror',
no_data = 'no_data',
}
export interface InframonitoringtypesClusterFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO[] | null;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesClusterRecordDTOCounts = {
/**
* @type integer
@@ -5964,6 +5923,21 @@ export interface InframonitoringtypesContainerCountsByStatusDTO {
waiting: number;
}
export enum InframonitoringtypesContainerReadyDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export type InframonitoringtypesContainerRecordDTOMetaAnyOf = {
[key: string]: string;
};
/**
* @nullable
*/
export type InframonitoringtypesContainerRecordDTOMeta =
InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
export enum InframonitoringtypesContainerStatusDTO {
running = 'running',
waiting = 'waiting',
@@ -5980,32 +5954,6 @@ export enum InframonitoringtypesContainerStatusDTO {
unknown = 'unknown',
no_data = 'no_data',
}
export interface InframonitoringtypesContainerFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByContainerStatus?: InframonitoringtypesContainerStatusDTO[] | null;
}
export enum InframonitoringtypesContainerReadyDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export type InframonitoringtypesContainerRecordDTOMetaAnyOf = {
[key: string]: string;
};
/**
* @nullable
*/
export type InframonitoringtypesContainerRecordDTOMeta =
InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
export interface InframonitoringtypesContainerRecordDTO {
containerCountsByReady: InframonitoringtypesContainerCountsByReadyDTO;
containerCountsByStatus: InframonitoringtypesContainerCountsByStatusDTO;
@@ -6077,17 +6025,6 @@ export interface InframonitoringtypesContainersDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesDaemonSetFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesDaemonSetRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6173,17 +6110,6 @@ export interface InframonitoringtypesDaemonSetsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesDeploymentFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesDeploymentRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6346,17 +6272,6 @@ export interface InframonitoringtypesHostsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesJobFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesJobRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6442,17 +6357,6 @@ export interface InframonitoringtypesJobsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesNamespaceFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesNamespaceRecordDTOCounts = {
/**
* @type integer
@@ -6529,21 +6433,11 @@ export interface InframonitoringtypesNamespacesDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesNodeFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO[] | null;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
export enum InframonitoringtypesNodeConditionDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export type InframonitoringtypesNodeRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6605,17 +6499,6 @@ export interface InframonitoringtypesNodesDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesPodFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6626,6 +6509,27 @@ export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
export type InframonitoringtypesPodRecordDTOMeta =
InframonitoringtypesPodRecordDTOMetaAnyOf | null;
export enum InframonitoringtypesPodStatusDTO {
pending = 'pending',
running = 'running',
failed = 'failed',
unknown = 'unknown',
crashloopbackoff = 'crashloopbackoff',
imagepullbackoff = 'imagepullbackoff',
errimagepull = 'errimagepull',
createcontainerconfigerror = 'createcontainerconfigerror',
containercreating = 'containercreating',
oomkilled = 'oomkilled',
completed = 'completed',
error = 'error',
containercannotrun = 'containercannotrun',
evicted = 'evicted',
nodeaffinity = 'nodeaffinity',
nodelost = 'nodelost',
shutdown = 'shutdown',
unexpectedadmissionerror = 'unexpectedadmissionerror',
no_data = 'no_data',
}
export interface InframonitoringtypesPodRecordDTO {
/**
* @type object,null
@@ -6702,7 +6606,7 @@ export interface InframonitoringtypesPostableClustersDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesClusterFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6729,7 +6633,7 @@ export interface InframonitoringtypesPostableContainersDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesContainerFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6756,7 +6660,7 @@ export interface InframonitoringtypesPostableDaemonSetsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesDaemonSetFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6783,7 +6687,7 @@ export interface InframonitoringtypesPostableDeploymentsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesDeploymentFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6837,7 +6741,7 @@ export interface InframonitoringtypesPostableJobsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesJobFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6864,7 +6768,7 @@ export interface InframonitoringtypesPostableNamespacesDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesNamespaceFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6891,7 +6795,7 @@ export interface InframonitoringtypesPostableNodesDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesNodeFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6918,7 +6822,7 @@ export interface InframonitoringtypesPostablePodsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesPodFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6939,24 +6843,13 @@ export interface InframonitoringtypesPostablePodsDTO {
start: number;
}
export interface InframonitoringtypesStatefulSetFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export interface InframonitoringtypesPostableStatefulSetsDTO {
/**
* @type integer
* @format int64
*/
end: number;
filter?: InframonitoringtypesStatefulSetFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -8965,112 +8858,6 @@ export interface RuletypesRuleDTO {
export enum RuletypesThresholdKindDTO {
basic = 'basic',
}
export interface SavedviewtypesDisplayDTO {
/**
* @type string
*/
color?: string;
/**
* @type string
*/
fontSize?: string;
/**
* @type string
*/
format?: string;
/**
* @type integer
*/
maxLines?: number;
}
export enum SavedviewtypesPanelTypeDTO {
value = 'value',
graph = 'graph',
table = 'table',
list = 'list',
trace = 'trace',
}
export interface SavedviewtypesSavedViewSpecDTO {
display: SavedviewtypesDisplayDTO;
/**
* @type string
*/
displayName: string;
panelType: SavedviewtypesPanelTypeDTO;
/**
* @type array
*/
queries: Querybuildertypesv5QueryEnvelopeDTO[];
/**
* @type array
*/
selectedFields: TelemetrytypesTelemetryFieldKeyDTO[];
}
export interface SavedviewtypesSavedViewDataDTO {
/**
* @type string
*/
schemaVersion: string;
spec: SavedviewtypesSavedViewSpecDTO;
}
export enum SavedviewtypesSourceDTO {
traces = 'traces',
logs = 'logs',
metrics = 'metrics',
meter = 'meter',
}
export interface SavedviewtypesPostableSavedViewDTO {
data: SavedviewtypesSavedViewDataDTO;
/**
* @type boolean
*/
generateName?: boolean;
/**
* @type string
*/
name?: string;
source: SavedviewtypesSourceDTO;
}
export interface SavedviewtypesSavedViewDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
data?: SavedviewtypesSavedViewDataDTO;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name?: string;
source?: SavedviewtypesSourceDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface SavedviewtypesUpdatableSavedViewDTO {
data: SavedviewtypesSavedViewDataDTO;
source: SavedviewtypesSourceDTO;
}
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {
/**
* @type string
@@ -10057,21 +9844,6 @@ export interface TypesOrganizationDTO {
updatedAt?: string;
}
export interface TypesPostableForgotPasswordDTO {
/**
* @type string
*/
email: string;
/**
* @type string
*/
frontendBaseURL?: string;
/**
* @type string
*/
orgId: string;
}
export interface TypesPostableInviteDTO {
/**
* @type string
@@ -10091,6 +9863,28 @@ export interface TypesPostableInviteDTO {
role?: string;
}
export interface TypesPostableBulkInviteRequestDTO {
/**
* @type array
*/
invites: TypesPostableInviteDTO[];
}
export interface TypesPostableForgotPasswordDTO {
/**
* @type string
*/
email: string;
/**
* @type string
*/
frontendBaseURL?: string;
/**
* @type string
*/
orgId: string;
}
export interface TypesPostableResetPasswordDTO {
/**
* @type string
@@ -11201,6 +10995,31 @@ export type ListUsersDeprecated200 = {
status: string;
};
export type DeleteUserDeprecatedPathParameters = {
id: string;
};
export type GetUserDeprecatedPathParameters = {
id: string;
};
export type GetUserDeprecated200 = {
data: TypesDeprecatedUserDTO;
/**
* @type string
*/
status: string;
};
export type UpdateUserDeprecatedPathParameters = {
id: string;
};
export type UpdateUserDeprecated200 = {
data: TypesDeprecatedUserDTO;
/**
* @type string
*/
status: string;
};
export type GetMyUserDeprecated200 = {
data: TypesDeprecatedUserDTO;
/**
@@ -12237,54 +12056,6 @@ export type TestRule200 = {
status: string;
};
export type ListSavedViewsParams = {
/**
* @description undefined
*/
source?: SavedviewtypesSourceDTO;
/**
* @type string
* @description undefined
*/
name?: string;
};
export type ListSavedViews200 = {
/**
* @type array,null
*/
data: SavedviewtypesSavedViewDTO[] | null;
/**
* @type string
*/
status: string;
};
export type CreateSavedView201 = {
data: TypesIdentifiableDTO;
/**
* @type string
*/
status: string;
};
export type DeleteSavedViewPathParameters = {
id: string;
};
export type GetSavedViewPathParameters = {
id: string;
};
export type GetSavedView200 = {
data: SavedviewtypesSavedViewDTO;
/**
* @type string
*/
status: string;
};
export type UpdateSavedViewPathParameters = {
id: string;
};
export type GetSessionContext200 = {
data: AuthtypesSessionContextDTO;
/**

View File

@@ -25,6 +25,7 @@ import type {
CreateResetPasswordTokenPathParameters,
CreateUser201,
CreateUserRole201,
DeleteUserDeprecatedPathParameters,
DeleteUserPathParameters,
DeleteUserRolePathParameters,
GetMyUser200,
@@ -36,6 +37,8 @@ import type {
GetRolesByUserID200,
GetRolesByUserIDPathParameters,
GetUser200,
GetUserDeprecated200,
GetUserDeprecatedPathParameters,
GetUserPathParameters,
GetUserRole200,
GetUserRolePathParameters,
@@ -47,12 +50,16 @@ import type {
RenderErrorResponseDTO,
SetRoleByUserIDPathParameters,
TypesChangePasswordRequestDTO,
TypesDeprecatedUserDTO,
TypesPostableBulkInviteRequestDTO,
TypesPostableForgotPasswordDTO,
TypesPostableInviteDTO,
TypesPostableResetPasswordDTO,
TypesPostableRoleDTO,
TypesPostableVerifyResetPasswordTokenDTO,
TypesUpdatableUserDTO,
UpdateUserDeprecated200,
UpdateUserDeprecatedPathParameters,
UpdateUserPathParameters,
} from '../sigNoz.schemas';
@@ -254,11 +261,95 @@ export const useCreateInvite = <
return useMutation(getCreateInviteMutationOptions(options));
};
/**
* This endpoint resets the password by token
* This endpoint creates a bulk invite for a user
* @deprecated
* @summary Create bulk invite
*/
export const createBulkInvite = (
typesPostableBulkInviteRequestDTO?: BodyType<TypesPostableBulkInviteRequestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/invite/bulk`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableBulkInviteRequestDTO,
signal,
});
};
export const getCreateBulkInviteMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
TContext
> => {
const mutationKey = ['createBulkInvite'];
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 createBulkInvite>>,
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> }
> = (props) => {
const { data } = props ?? {};
return createBulkInvite(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateBulkInviteMutationResult = NonNullable<
Awaited<ReturnType<typeof createBulkInvite>>
>;
export type CreateBulkInviteMutationBody =
| BodyType<TypesPostableBulkInviteRequestDTO>
| undefined;
export type CreateBulkInviteMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Create bulk invite
*/
export const useCreateBulkInvite = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
TContext
> => {
return useMutation(getCreateBulkInviteMutationOptions(options));
};
/**
* This endpoint resets the password by token
* @summary Reset password
*/
export const resetPasswordDeprecated = (
export const resetPassword = (
typesPostableResetPasswordDTO?: BodyType<TypesPostableResetPasswordDTO>,
signal?: AbortSignal,
) => {
@@ -271,23 +362,23 @@ export const resetPasswordDeprecated = (
});
};
export const getResetPasswordDeprecatedMutationOptions = <
export const getResetPasswordMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
> => {
const mutationKey = ['resetPasswordDeprecated'];
const mutationKey = ['resetPassword'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
@@ -297,47 +388,45 @@ export const getResetPasswordDeprecatedMutationOptions = <
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
Awaited<ReturnType<typeof resetPassword>>,
{ data?: BodyType<TypesPostableResetPasswordDTO> }
> = (props) => {
const { data } = props ?? {};
return resetPasswordDeprecated(data);
return resetPassword(data);
};
return { mutationFn, ...mutationOptions };
};
export type ResetPasswordDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof resetPasswordDeprecated>>
export type ResetPasswordMutationResult = NonNullable<
Awaited<ReturnType<typeof resetPassword>>
>;
export type ResetPasswordDeprecatedMutationBody =
export type ResetPasswordMutationBody =
| BodyType<TypesPostableResetPasswordDTO>
| undefined;
export type ResetPasswordDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
export type ResetPasswordMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Reset password
*/
export const useResetPasswordDeprecated = <
export const useResetPassword = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
> => {
return useMutation(getResetPasswordDeprecatedMutationOptions(options));
return useMutation(getResetPasswordMutationOptions(options));
};
/**
* This endpoint lists all users
@@ -426,6 +515,295 @@ export const invalidateListUsersDeprecated = async (
return queryClient;
};
/**
* This endpoint deletes the user by id
* @deprecated
* @summary Delete user
*/
export const deleteUserDeprecated = (
{ id }: DeleteUserDeprecatedPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/user/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteUserDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUserDeprecated>>,
TError,
{ pathParams: DeleteUserDeprecatedPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteUserDeprecated>>,
TError,
{ pathParams: DeleteUserDeprecatedPathParameters },
TContext
> => {
const mutationKey = ['deleteUserDeprecated'];
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 deleteUserDeprecated>>,
{ pathParams: DeleteUserDeprecatedPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteUserDeprecated(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteUserDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteUserDeprecated>>
>;
export type DeleteUserDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Delete user
*/
export const useDeleteUserDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUserDeprecated>>,
TError,
{ pathParams: DeleteUserDeprecatedPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteUserDeprecated>>,
TError,
{ pathParams: DeleteUserDeprecatedPathParameters },
TContext
> => {
return useMutation(getDeleteUserDeprecatedMutationOptions(options));
};
/**
* This endpoint returns the user by id
* @deprecated
* @summary Get user
*/
export const getUserDeprecated = (
{ id }: GetUserDeprecatedPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetUserDeprecated200>({
url: `/api/v1/user/${id}`,
method: 'GET',
signal,
});
};
export const getGetUserDeprecatedQueryKey = ({
id,
}: GetUserDeprecatedPathParameters) => {
return [`/api/v1/user/${id}`] as const;
};
export const getGetUserDeprecatedQueryOptions = <
TData = Awaited<ReturnType<typeof getUserDeprecated>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetUserDeprecatedPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getUserDeprecated>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetUserDeprecatedQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getUserDeprecated>>
> = ({ signal }) => getUserDeprecated({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getUserDeprecated>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetUserDeprecatedQueryResult = NonNullable<
Awaited<ReturnType<typeof getUserDeprecated>>
>;
export type GetUserDeprecatedQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Get user
*/
export function useGetUserDeprecated<
TData = Awaited<ReturnType<typeof getUserDeprecated>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetUserDeprecatedPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getUserDeprecated>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetUserDeprecatedQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @deprecated
* @summary Get user
*/
export const invalidateGetUserDeprecated = async (
queryClient: QueryClient,
{ id }: GetUserDeprecatedPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetUserDeprecatedQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint updates the user by id
* @deprecated
* @summary Update user
*/
export const updateUserDeprecated = (
{ id }: UpdateUserDeprecatedPathParameters,
typesDeprecatedUserDTO?: BodyType<TypesDeprecatedUserDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<UpdateUserDeprecated200>({
url: `/api/v1/user/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: typesDeprecatedUserDTO,
signal,
});
};
export const getUpdateUserDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateUserDeprecated>>,
TError,
{
pathParams: UpdateUserDeprecatedPathParameters;
data?: BodyType<TypesDeprecatedUserDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateUserDeprecated>>,
TError,
{
pathParams: UpdateUserDeprecatedPathParameters;
data?: BodyType<TypesDeprecatedUserDTO>;
},
TContext
> => {
const mutationKey = ['updateUserDeprecated'];
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 updateUserDeprecated>>,
{
pathParams: UpdateUserDeprecatedPathParameters;
data?: BodyType<TypesDeprecatedUserDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateUserDeprecated(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateUserDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof updateUserDeprecated>>
>;
export type UpdateUserDeprecatedMutationBody =
| BodyType<TypesDeprecatedUserDTO>
| undefined;
export type UpdateUserDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Update user
*/
export const useUpdateUserDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateUserDeprecated>>,
TError,
{
pathParams: UpdateUserDeprecatedPathParameters;
data?: BodyType<TypesDeprecatedUserDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateUserDeprecated>>,
TError,
{
pathParams: UpdateUserDeprecatedPathParameters;
data?: BodyType<TypesDeprecatedUserDTO>;
},
TContext
> => {
return useMutation(getUpdateUserDeprecatedMutationOptions(options));
};
/**
* This endpoint returns the user I belong to
* @deprecated
@@ -596,89 +974,6 @@ export const useForgotPassword = <
> => {
return useMutation(getForgotPasswordMutationOptions(options));
};
/**
* This endpoint resets the password using a single use reset password token
* @summary Reset password
*/
export const resetPassword = (
typesPostableResetPasswordDTO?: BodyType<TypesPostableResetPasswordDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/factor_password/reset`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableResetPasswordDTO,
signal,
});
};
export const getResetPasswordMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
> => {
const mutationKey = ['resetPassword'];
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 resetPassword>>,
{ data?: BodyType<TypesPostableResetPasswordDTO> }
> = (props) => {
const { data } = props ?? {};
return resetPassword(data);
};
return { mutationFn, ...mutationOptions };
};
export type ResetPasswordMutationResult = NonNullable<
Awaited<ReturnType<typeof resetPassword>>
>;
export type ResetPasswordMutationBody =
| BodyType<TypesPostableResetPasswordDTO>
| undefined;
export type ResetPasswordMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Reset password
*/
export const useResetPassword = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
> => {
return useMutation(getResetPasswordMutationOptions(options));
};
/**
* This endpoint verifies whether a reset password token exists and is not expired
* @summary Verify a reset password token

View File

@@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { UsersProps } from 'types/api/user/inviteUsers';
/**
* @deprecated Use the generated `useCreateBulkInvite` hook (or `createBulkInvite` fetcher) from
* `api/generated/services/users` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const inviteUsers = async (
users: UsersProps,
): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.post(`/invite/bulk`, users);
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default inviteUsers;

View File

@@ -80,7 +80,7 @@ function InviteMembers({
weight="semibold"
className={styles.headerCellRole}
>
Roles
Role
</Typography.Text>
<div className={styles.headerCellAction} />
</div>
@@ -108,10 +108,11 @@ function InviteMembers({
<div className={styles.cellRole}>
<RolesSelect
mode="multiple"
value={row.roleIds}
onChange={(roleIds): void => updateRole(row.id, roleIds)}
placeholder="Select roles"
mode="single"
value={row.roleId || undefined}
onChange={(roleId): void => updateRole(row.id, roleId)}
placeholder="Select role"
allowClear={false}
id={`invite-role-${row.id}`}
/>
</div>

View File

@@ -68,8 +68,8 @@ describe('InviteMembers - Edge Cases', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
await expect(
@@ -100,8 +100,8 @@ describe('InviteMembers - Edge Cases', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
await expect(
@@ -132,17 +132,17 @@ describe('InviteMembers - Edge Cases', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
await expect(
screen.findByTestId('invite-api-error'),
).resolves.toBeInTheDocument();
const viewerElements = screen.getAllByTitle('Viewer');
const viewerElements = screen.getAllByText('Viewer');
await user.click(viewerElements[0]);
const editorOptions = await screen.findAllByTitle('Editor');
const editorOptions = await screen.findAllByText('Editor');
await user.click(editorOptions[editorOptions.length - 1]);
await waitFor(() => {
@@ -189,8 +189,8 @@ describe('InviteMembers - Edge Cases', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
const submitBtn = screen.getByTestId('submit-btn');
await user.click(submitBtn);
@@ -226,8 +226,8 @@ describe('InviteMembers - Edge Cases', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], ' alice@signoz.io ');
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));

View File

@@ -32,14 +32,14 @@ describe('InviteMembers - Rendering', () => {
render(<InviteMembers />);
expect(screen.getByText('Email address')).toBeInTheDocument();
expect(screen.getByText('Roles')).toBeInTheDocument();
expect(screen.getByText('Role')).toBeInTheDocument();
});
it('hides header when showHeader is false', () => {
render(<InviteMembers showHeader={false} />);
expect(screen.queryByText('Email address')).not.toBeInTheDocument();
expect(screen.queryByText('Roles')).not.toBeInTheDocument();
expect(screen.queryByText('Role')).not.toBeInTheDocument();
});
it('renders add button by default', () => {
@@ -89,7 +89,7 @@ describe('InviteMembers - Rendering', () => {
it('renders role select for each row', () => {
render(<InviteMembers initialRowCount={2} />);
const roleSelects = screen.getAllByText('Select roles');
const roleSelects = screen.getAllByText('Select role');
expect(roleSelects).toHaveLength(2);
});
});

View File

@@ -40,8 +40,8 @@ describe('InviteMembers - Submission', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], 'alice@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
@@ -73,17 +73,17 @@ describe('InviteMembers - Submission', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], 'alice@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.type(emailInputs[1], 'bob@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
const editorOptions = await screen.findAllByTitle('Editor');
await user.click(screen.getAllByText('Select role')[0]);
const editorOptions = await screen.findAllByText('Editor');
await user.click(editorOptions[editorOptions.length - 1]);
await user.type(emailInputs[2], 'charlie@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
const adminOptions = await screen.findAllByTitle('Admin');
await user.click(screen.getAllByText('Select role')[0]);
const adminOptions = await screen.findAllByText('Admin');
await user.click(adminOptions[adminOptions.length - 1]);
await user.click(screen.getByTestId('submit-btn'));
@@ -125,8 +125,8 @@ describe('InviteMembers - Submission', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
@@ -154,8 +154,8 @@ describe('InviteMembers - Submission', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
@@ -218,12 +218,12 @@ describe('InviteMembers - Submission', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], 'alice@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.type(emailInputs[1], 'bob@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
const editorOptions = await screen.findAllByTitle('Editor');
await user.click(screen.getAllByText('Select role')[0]);
const editorOptions = await screen.findAllByText('Editor');
await user.click(editorOptions[editorOptions.length - 1]);
await user.click(screen.getByTestId('submit-btn'));
@@ -276,8 +276,8 @@ describe('InviteMembers - Submission', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
@@ -303,8 +303,8 @@ describe('InviteMembers - Submission', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));

View File

@@ -35,8 +35,8 @@ describe('InviteMembers - Validation', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], INVALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
@@ -60,8 +60,8 @@ describe('InviteMembers - Validation', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], INVALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
@@ -85,8 +85,8 @@ describe('InviteMembers - Validation', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], INVALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
await expect(
@@ -149,8 +149,8 @@ describe('InviteMembers - Validation', () => {
screen.findByText('Please select roles for team members'),
).resolves.toBeInTheDocument();
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await waitFor(() => {
expect(
@@ -204,8 +204,8 @@ describe('InviteMembers - Validation', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));

View File

@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
export interface InviteMemberRow {
id: string;
email: string;
roleIds: string[];
roleId: string;
}
export interface InviteResult {
@@ -38,7 +38,7 @@ export interface UseInviteMembersReturn {
addRow: () => void;
removeRow: (id: string) => void;
updateEmail: (id: string, email: string) => void;
updateRole: (id: string, roleIds: string[]) => void;
updateRole: (id: string, roleId: string | undefined) => void;
reset: () => void;
submit: () => Promise<InviteResult[]>;

View File

@@ -18,11 +18,11 @@ import {
const createEmptyRow = (): InviteMemberRow => ({
id: uuid(),
email: '',
roleIds: [],
roleId: '',
});
const isRowTouched = (row: InviteMemberRow): boolean =>
row.email.trim() !== '' || row.roleIds.length > 0;
row.email.trim() !== '' || row.roleId !== '';
export function useInviteMembers(
options: UseInviteMembersOptions = {},
@@ -78,7 +78,7 @@ export function useInviteMembers(
touched.forEach((row) => {
const emailValid = EMAIL_REGEX.test(row.email);
const roleValid = row.roleIds.length > 0;
const roleValid = row.roleId !== '';
if (!emailValid || !row.email) {
isValid = false;
@@ -139,12 +139,12 @@ export function useInviteMembers(
);
const updateRole = useCallback(
(id: string, roleIds: string[]): void => {
(id: string, roleId: string | undefined): void => {
setRows((prev) => {
const updated = cloneDeep(prev);
const row = updated.find((r) => r.id === id);
if (row) {
row.roleIds = roleIds;
row.roleId = roleId ?? '';
}
return updated;
});
@@ -187,7 +187,7 @@ export function useInviteMembers(
await createUser({
email: row.email.trim(),
frontendBaseUrl: getBaseUrl(),
userRoles: row.roleIds.map((id) => ({ id })),
userRoles: [{ id: row.roleId }],
});
results.push({ email: row.email, success: true });
} catch (err) {

View File

@@ -1,49 +0,0 @@
.header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
gap: 8px;
}
.tooltipContent {
--tooltip-z-index: 2100;
}
.dropdownContent {
--dropdown-menu-content-z-index: 2100;
}
.leftSection {
display: flex;
align-items: center;
gap: 8px;
}
.divider {
height: 16px;
margin: 0;
}
.timestamp {
font-family: 'Geist Mono', monospace;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
color: var(--l1-foreground);
letter-spacing: -0.07px;
}
.actions {
display: flex;
align-items: center;
gap: 8px;
}
.arrows {
display: flex;
align-items: center;
gap: 2px;
padding: 2px 6px;
border-radius: 6px;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1);
}

View File

@@ -1,153 +0,0 @@
import { Button } from '@signozhq/ui/button';
import { Divider } from '@signozhq/ui/divider';
import { DropdownMenuSimple as Dropdown } from '@signozhq/ui/dropdown-menu';
import { Typography } from '@signozhq/ui/typography';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
import { toast } from '@signozhq/ui/sonner';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import {
ChevronDown,
ChevronUp,
Compass,
Copy,
Ellipsis,
Link,
} from '@signozhq/icons';
import { useTimezone } from 'providers/Timezone';
import { ILog } from 'types/api/logs/log';
import { MouseEvent, MouseEventHandler } from 'react';
import { useCopyToClipboard } from 'react-use';
import styles from './LogDetailsHeader.module.scss';
const TOOLTIP_CONTENT_PROPS = { className: styles.tooltipContent };
interface LogDetailsHeaderProps {
log: ILog;
onNavigatePrev: () => void;
onNavigateNext: () => void;
isPrevDisabled: boolean;
isNextDisabled: boolean;
showOpenInExplorer?: boolean;
onOpenInExplorer?: MouseEventHandler;
}
function LogDetailsHeader({
log,
onNavigatePrev,
onNavigateNext,
isPrevDisabled,
isNextDisabled,
showOpenInExplorer = false,
onOpenInExplorer,
}: LogDetailsHeaderProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const { onLogCopy } = useCopyLogLink(log?.id);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const handleCopyLog = (): void => {
copyToClipboard(aggregateAttributesResourcesToString(log));
toast.success('Copied to clipboard', { position: 'bottom-right' });
};
const menuItems = [
{
key: 'copy-log',
label: 'Copy log',
icon: <Copy size={14} />,
onClick: handleCopyLog,
},
{
key: 'copy-link',
label: 'Copy link to log',
icon: <Link size={14} />,
onClick: (): void => onLogCopy(),
},
];
return (
<div className={styles.header} data-log-detail-ignore="true">
<div className={styles.leftSection}>
<Divider type="vertical" className={styles.divider} />
<Typography.Text
className={styles.timestamp}
data-testid="log-details-header-timestamp"
>
{formatTimezoneAdjustedTimestamp(
log.date ?? log.timestamp,
DATE_TIME_FORMATS.DASH_DATETIME,
)}
</Typography.Text>
</div>
<div className={styles.actions}>
{showOpenInExplorer && (
<Button
variant="outlined"
color="secondary"
prefix={<Compass size={16} />}
onClick={onOpenInExplorer}
>
Open in Explorer
</Button>
)}
<Dropdown
menu={{ items: menuItems }}
align="end"
className={styles.dropdownContent}
onClick={(e: MouseEvent): void => e.stopPropagation()}
>
<Button
variant="link"
color="secondary"
prefix={<Ellipsis size={16} />}
data-testid="log-details-header-menu"
/>
</Dropdown>
<div className={styles.arrows}>
<TooltipSimple
title="Move to previous log"
side="top"
open={isPrevDisabled ? false : undefined}
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
>
<Button
variant="outlined"
color="secondary"
prefix={<ChevronUp size={14} />}
disabled={isPrevDisabled}
onClick={onNavigatePrev}
data-testid="log-details-header-prev"
/>
</TooltipSimple>
<TooltipSimple
title="Move to next log"
side="top"
open={isNextDisabled ? false : undefined}
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
>
<Button
variant="outlined"
color="secondary"
prefix={<ChevronDown size={14} />}
disabled={isNextDisabled}
onClick={onNavigateNext}
data-testid="log-details-header-next"
/>
</TooltipSimple>
</div>
</div>
</div>
);
}
LogDetailsHeader.defaultProps = {
showOpenInExplorer: false,
onOpenInExplorer: undefined,
};
export default LogDetailsHeader;

View File

@@ -1,52 +0,0 @@
import { useCallback, useMemo } from 'react';
import { ILog } from 'types/api/logs/log';
interface UseLogNavigationParams {
logs?: ILog[];
activeLogId: string;
onNavigateLog?: (log: ILog) => void;
onScrollToLog?: (id: string) => void;
}
interface UseLogNavigationReturn {
goToPrev: () => void;
goToNext: () => void;
isPrevDisabled: boolean;
isNextDisabled: boolean;
}
export function useLogNavigation({
logs,
activeLogId,
onNavigateLog,
onScrollToLog,
}: UseLogNavigationParams): UseLogNavigationReturn {
const currentIndex = useMemo(
() => logs?.findIndex((l) => l.id === activeLogId) ?? -1,
[logs, activeLogId],
);
const canNavigate = !!logs?.length && !!onNavigateLog && currentIndex !== -1;
const isPrevDisabled = !canNavigate || currentIndex <= 0;
const isNextDisabled = !canNavigate || currentIndex >= (logs?.length ?? 0) - 1;
const goToPrev = useCallback((): void => {
if (isPrevDisabled || !logs) {
return;
}
const prev = logs[currentIndex - 1];
onNavigateLog?.(prev);
onScrollToLog?.(prev.id);
}, [isPrevDisabled, logs, currentIndex, onNavigateLog, onScrollToLog]);
const goToNext = useCallback((): void => {
if (isNextDisabled || !logs) {
return;
}
const next = logs[currentIndex + 1];
onNavigateLog?.(next);
onScrollToLog?.(next.id);
}, [isNextDisabled, logs, currentIndex, onNavigateLog, onScrollToLog]);
return { goToPrev, goToNext, isPrevDisabled, isNextDisabled };
}

View File

@@ -1,161 +0,0 @@
import { toast } from '@signozhq/ui/sonner';
import { LOCALSTORAGE } from 'constants/localStorage';
import { render, screen, userEvent } from 'tests/test-utils';
import { ILog } from 'types/api/logs/log';
import LogDetail from '..';
import { VIEW_TYPES } from '../constants';
import { LogDetailProps } from '../LogDetail.interfaces';
jest.mock('@signozhq/ui/sonner', () => ({
toast: { success: jest.fn(), error: jest.fn() },
}));
// The flag to be removed later
jest.mock('../constants', () => ({
...jest.requireActual('../constants'),
isLogDetailsV2: true,
}));
const mockLog: ILog = {
id: 'log-1',
timestamp: '2024-01-15T09:45:30Z',
date: '2024-01-15T09:45:30Z',
body: 'test log body',
severityText: 'INFO',
severityNumber: 9,
traceFlags: 0,
traceId: '',
spanID: '',
attributesString: {},
attributesInt: {},
attributesFloat: {},
resources_string: {},
scope_string: {},
attributes_string: {},
severity_text: 'INFO',
severity_number: 9,
};
const makeLog = (id: string): ILog => ({ ...mockLog, id });
function renderDrawer(props: Partial<LogDetailProps> = {}): void {
render(
<LogDetail
log={mockLog}
selectedTab={VIEW_TYPES.OVERVIEW}
onAddToQuery={jest.fn()}
onClickActionItem={jest.fn()}
onClose={jest.fn()}
{...props}
/>,
);
}
describe('LogDetail drawer — header (isLogDetailsV2)', () => {
afterEach(() => {
jest.clearAllMocks();
localStorage.clear();
});
it('renders the revamped header when a log is provided', () => {
renderDrawer();
expect(screen.getByTestId('log-details-header-menu')).toBeInTheDocument();
expect(screen.getByTestId('log-details-header-prev')).toBeInTheDocument();
expect(screen.getByTestId('log-details-header-next')).toBeInTheDocument();
});
it('shows the log timestamp formatted (DASH_DATETIME) in the header', () => {
// Pin the timezone to UTC so the formatted output is deterministic across
// machines/CI (Jest doesn't fix a TZ).
localStorage.setItem(LOCALSTORAGE.PREFERRED_TIMEZONE, 'UTC');
renderDrawer();
// mockLog date is 2024-01-15T09:45:30Z → DASH_DATETIME in UTC.
expect(screen.getByTestId('log-details-header-timestamp')).toHaveTextContent(
'Jan 15, 2024 ⎯ 09:45:30',
);
});
it('copies the log link from the ⋯ menu', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderDrawer();
await user.click(screen.getByTestId('log-details-header-menu'));
await user.click(await screen.findByText('Copy link to log'));
expect(toast.success).toHaveBeenCalled();
});
it('copies the log from the ⋯ menu', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderDrawer();
await user.click(screen.getByTestId('log-details-header-menu'));
await user.click(await screen.findByText('Copy log'));
expect(toast.success).toHaveBeenCalledWith('Copied to clipboard', {
position: 'bottom-right',
});
});
it('shows "Open in Explorer" when a handleOpenInExplorer handler is provided', () => {
renderDrawer({ handleOpenInExplorer: jest.fn() });
expect(screen.getByText('Open in Explorer')).toBeInTheDocument();
});
it('hides "Open in Explorer" when no handleOpenInExplorer handler is provided', () => {
renderDrawer();
expect(screen.queryByText('Open in Explorer')).not.toBeInTheDocument();
});
it('navigates to the next / previous log with the Down / Up arrow keys', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const logs = [makeLog('log-0'), makeLog('log-1'), makeLog('log-2')];
const onNavigateLog = jest.fn();
const onScrollToLog = jest.fn();
// Active log is the middle one so both directions are available.
renderDrawer({ log: logs[1], logs, onNavigateLog, onScrollToLog });
await user.keyboard('{ArrowDown}');
expect(onNavigateLog).toHaveBeenLastCalledWith(logs[2]);
expect(onScrollToLog).toHaveBeenLastCalledWith('log-2');
await user.keyboard('{ArrowUp}');
expect(onNavigateLog).toHaveBeenLastCalledWith(logs[0]);
expect(onScrollToLog).toHaveBeenLastCalledWith('log-0');
});
it('does not navigate past the first log on ArrowUp', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const logs = [makeLog('log-0'), makeLog('log-1')];
const onNavigateLog = jest.fn();
renderDrawer({ log: logs[0], logs, onNavigateLog });
await user.keyboard('{ArrowUp}');
expect(onNavigateLog).not.toHaveBeenCalled();
});
it('navigates via the header up / down buttons and disables them at boundaries', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const logs = [makeLog('log-0'), makeLog('log-1')];
const onNavigateLog = jest.fn();
// Active log is the first one.
renderDrawer({ log: logs[0], logs, onNavigateLog });
expect(screen.getByTestId('log-details-header-prev')).toBeDisabled();
expect(screen.getByTestId('log-details-header-next')).toBeEnabled();
await user.click(screen.getByTestId('log-details-header-next'));
expect(onNavigateLog).toHaveBeenLastCalledWith(logs[1]);
});
});

View File

@@ -1,10 +1,3 @@
import getLocalStorage from 'api/browser/localstorage/get';
import { LOCALSTORAGE } from 'constants/localStorage';
// Temp feature flag before actual roll-out
export const isLogDetailsV2 =
getLocalStorage(LOCALSTORAGE.LOG_DETAILS_V2) === 'true';
export const VIEW_TYPES = {
OVERVIEW: 'OVERVIEW',
JSON: 'JSON',

View File

@@ -8,9 +8,7 @@ import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import LogStateIndicator, {
LogType,
} from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
import { FeatureKeys } from 'constants/features';
@@ -25,7 +23,6 @@ import {
} from 'container/LogDetailedView/utils';
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
import { useOptionsMenu } from 'container/OptionsMenu';
import { FontSize } from 'container/OptionsMenu/types';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -51,10 +48,8 @@ import { ILogBody } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { isLogDetailsV2, RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
import LogDetailsHeader from './LogDetailsHeader/LogDetailsHeader';
import { useLogNavigation } from './LogDetailsHeader/useLogNavigation';
import './LogDetails.styles.scss';
@@ -101,8 +96,7 @@ function LogDetailInner({
target.closest('[data-log-detail-ignore="true"]') ||
target.closest('.cm-tooltip-autocomplete') ||
target.closest('.drawer-popover') ||
target.closest('.query-status-popover') ||
target.closest('[data-radix-popper-content-wrapper]')
target.closest('.query-status-popover')
) {
return;
}
@@ -118,30 +112,49 @@ function LogDetailInner({
};
}, [onClose]);
const { goToPrev, goToNext, isPrevDisabled, isNextDisabled } =
useLogNavigation({
logs,
activeLogId: log.id,
onNavigateLog,
onScrollToLog,
});
// Keyboard navigation - handle up/down arrow keys. Only listen in the OVERVIEW
// tab so we don't hijack arrow keys from the JSON editor / context view.
// Keyboard navigation - handle up/down arrow keys
// Only listen when in OVERVIEW tab
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
if (selectedView !== VIEW_TYPES.OVERVIEW) {
return undefined;
if (
!logs ||
!onNavigateLog ||
logs.length === 0 ||
selectedView !== VIEW_TYPES.OVERVIEW
) {
return;
}
const handleKeyDown = (e: KeyboardEvent): void => {
const currentIndex = logs.findIndex((l) => l.id === log.id);
if (currentIndex === -1) {
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
e.stopPropagation();
goToPrev();
// Navigate to previous log
if (currentIndex > 0) {
const prevLog = logs[currentIndex - 1];
onNavigateLog(prevLog);
// Trigger scroll to the log element
if (onScrollToLog) {
onScrollToLog(prevLog.id);
}
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
e.stopPropagation();
goToNext();
// Navigate to next log
if (currentIndex < logs.length - 1) {
const nextLog = logs[currentIndex + 1];
onNavigateLog(nextLog);
// Trigger scroll to the log element
if (onScrollToLog) {
onScrollToLog(nextLog.id);
}
}
}
};
@@ -149,7 +162,7 @@ function LogDetailInner({
return (): void => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [selectedView, goToPrev, goToNext]);
}, [log.id, logs, onNavigateLog, onScrollToLog, selectedView]);
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) {
@@ -290,6 +303,33 @@ function LogDetailInner({
};
const logType = log?.attributes_string?.log_level || LogType.INFO;
const currentLogIndex = logs ? logs.findIndex((l) => l.id === log.id) : -1;
const isPrevDisabled =
!logs || !onNavigateLog || logs.length === 0 || currentLogIndex <= 0;
const isNextDisabled =
!logs ||
!onNavigateLog ||
logs.length === 0 ||
currentLogIndex === logs.length - 1;
type HandleNavigateLogParams = {
direction: 'next' | 'previous';
};
const handleNavigateLog = ({ direction }: HandleNavigateLogParams): void => {
if (!logs || !onNavigateLog || currentLogIndex === -1) {
return;
}
if (direction === 'previous' && !isPrevDisabled) {
const prevLog = logs[currentLogIndex - 1];
onNavigateLog(prevLog);
onScrollToLog?.(prevLog.id);
} else if (direction === 'next' && !isNextDisabled) {
const nextLog = logs[currentLogIndex + 1];
onNavigateLog(nextLog);
onScrollToLog?.(nextLog.id);
}
};
return (
<Drawer
@@ -298,69 +338,57 @@ function LogDetailInner({
maskClosable={false}
getContainer={getContainer}
title={
isLogDetailsV2 ? (
<LogDetailsHeader
log={log}
onNavigatePrev={goToPrev}
onNavigateNext={goToNext}
isPrevDisabled={isPrevDisabled}
isNextDisabled={isNextDisabled}
showOpenInExplorer={!!handleOpenInExplorer}
onOpenInExplorer={handleOpenInExplorer}
/>
) : (
<div className="log-detail-drawer__title" data-log-detail-ignore="true">
<div className="log-detail-drawer__title-left">
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
<Typography.Text className="title">Log details</Typography.Text>
</div>
<div className="log-detail-drawer__title-right">
<div className="log-arrows">
<Tooltip
title={isPrevDisabled ? '' : 'Move to previous log'}
placement="top"
mouseLeaveDelay={0}
>
<Button
variant="outlined"
color="secondary"
prefix={<ChevronUp size={14} />}
className="log-arrow-btn log-arrow-btn-up"
disabled={isPrevDisabled}
onClick={goToPrev}
/>
</Tooltip>
<Tooltip
title={isNextDisabled ? '' : 'Move to next log'}
placement="top"
mouseLeaveDelay={0}
>
<Button
variant="outlined"
color="secondary"
prefix={<ChevronDown size={14} />}
className="log-arrow-btn log-arrow-btn-down"
disabled={isNextDisabled}
onClick={goToNext}
/>
</Tooltip>
</div>
{handleOpenInExplorer && (
<div>
<Button
variant="outlined"
color="secondary"
prefix={<Compass size={16} />}
className="open-in-explorer-btn"
onClick={handleOpenInExplorer}
>
Open in Explorer
</Button>
</div>
)}
</div>
<div className="log-detail-drawer__title" data-log-detail-ignore="true">
<div className="log-detail-drawer__title-left">
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
<Typography.Text className="title">Log details</Typography.Text>
</div>
)
<div className="log-detail-drawer__title-right">
<div className="log-arrows">
<Tooltip
title={isPrevDisabled ? '' : 'Move to previous log'}
placement="top"
mouseLeaveDelay={0}
>
<Button
variant="outlined"
color="secondary"
prefix={<ChevronUp size={14} />}
className="log-arrow-btn log-arrow-btn-up"
disabled={isPrevDisabled}
onClick={(): void => handleNavigateLog({ direction: 'previous' })}
/>
</Tooltip>
<Tooltip
title={isNextDisabled ? '' : 'Move to next log'}
placement="top"
mouseLeaveDelay={0}
>
<Button
variant="outlined"
color="secondary"
prefix={<ChevronDown size={14} />}
className="log-arrow-btn log-arrow-btn-down"
disabled={isNextDisabled}
onClick={(): void => handleNavigateLog({ direction: 'next' })}
/>
</Tooltip>
</div>
{handleOpenInExplorer && (
<div>
<Button
variant="outlined"
color="secondary"
prefix={<Compass size={16} />}
className="open-in-explorer-btn"
onClick={handleOpenInExplorer}
>
Open in Explorer
</Button>
</div>
)}
</div>
</div>
}
placement="right"
onClose={drawerCloseHandler}
@@ -379,15 +407,7 @@ function LogDetailInner({
data-testid="log-detail-drawer"
>
<div className="log-detail-drawer__log">
{isLogDetailsV2 ? (
<LogStateIndicator
severityText={log.severity_text}
severityNumber={log.severity_number}
fontSize={options?.fontSize ?? FontSize.MEDIUM}
/>
) : (
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
)}
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip
title={removeEscapeCharacters(logBody)}
placement="left"
@@ -463,25 +483,22 @@ function LogDetailInner({
</Tooltip>
)}
{/* V2 moves copy actions into the header ⋯ menu */}
{!isLogDetailsV2 && (
<Tooltip
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
placement="topLeft"
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
mouseLeaveDelay={0}
>
<Button
variant="link"
color="secondary"
size="sm"
prefix={<Copy size={12} />}
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
/>
</Tooltip>
)}
<Tooltip
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
placement="topLeft"
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
mouseLeaveDelay={0}
>
<Button
variant="link"
color="secondary"
size="sm"
prefix={<Copy size={12} />}
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
/>
</Tooltip>
</div>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (

View File

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

View File

@@ -13,7 +13,6 @@ export enum LOCALSTORAGE {
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
LOGS_LIST_COLUMN_SIZING = 'LOGS_LIST_COLUMN_SIZING',
LOG_DETAILS_V2 = 'LOG_DETAILS_V2',
LOGGED_IN_USER_NAME = 'LOGGED_IN_USER_NAME',
LOGGED_IN_USER_EMAIL = 'LOGGED_IN_USER_EMAIL',
CHAT_SUPPORT = 'CHAT_SUPPORT',

View File

@@ -14,7 +14,7 @@ import './InviteTeamMembers.styles.scss';
interface TeamMember {
email: string;
roles: string[];
role: string;
name: string;
frontendBaseUrl: string;
id: string;
@@ -45,7 +45,7 @@ function InviteTeamMembers({
const toTeamMembers = (rows: InviteMemberRow[]): TeamMember[] =>
rows.map((row) => ({
email: row.email,
roles: row.roleIds.map((roleId) => roleIdToName[roleId] ?? roleId),
role: roleIdToName[row.roleId] ?? row.roleId,
name: '',
frontendBaseUrl: getBaseUrl(),
id: row.id,

View File

@@ -166,8 +166,8 @@ describe('InviteTeamMembers', () => {
{ email: 'user2@test.com', success: true },
];
const mockRows: InviteMemberRow[] = [
{ id: 'row-1', email: 'user1@test.com', roleIds: ['role-viewer-id'] },
{ id: 'row-2', email: 'user2@test.com', roleIds: ['role-editor-id'] },
{ id: 'row-1', email: 'user1@test.com', roleId: 'role-viewer-id' },
{ id: 'row-2', email: 'user2@test.com', roleId: 'role-editor-id' },
];
mockInviteMembersProps?.onSuccess?.(mockResults, mockRows);
@@ -177,14 +177,14 @@ describe('InviteTeamMembers', () => {
teamMembers: [
{
email: 'user1@test.com',
roles: ['VIEWER'],
role: 'VIEWER',
name: '',
frontendBaseUrl: 'http://localhost:3301',
id: 'row-1',
},
{
email: 'user2@test.com',
roles: ['EDITOR'],
role: 'EDITOR',
name: '',
frontendBaseUrl: 'http://localhost:3301',
id: 'row-2',
@@ -211,8 +211,8 @@ describe('InviteTeamMembers', () => {
{ email: 'user2@test.com', success: false, error: 'Already exists' },
];
const mockRows: InviteMemberRow[] = [
{ id: 'row-1', email: 'user1@test.com', roleIds: ['role-viewer-id'] },
{ id: 'row-2', email: 'user2@test.com', roleIds: ['role-admin-id'] },
{ id: 'row-1', email: 'user1@test.com', roleId: 'role-viewer-id' },
{ id: 'row-2', email: 'user2@test.com', roleId: 'role-admin-id' },
];
mockInviteMembersProps?.onPartialSuccess?.(mockResults, mockRows);
@@ -222,14 +222,14 @@ describe('InviteTeamMembers', () => {
teamMembers: [
{
email: 'user1@test.com',
roles: ['VIEWER'],
role: 'VIEWER',
name: '',
frontendBaseUrl: 'http://localhost:3301',
id: 'row-1',
},
{
email: 'user2@test.com',
roles: ['ADMIN'],
role: 'ADMIN',
name: '',
frontendBaseUrl: 'http://localhost:3301',
id: 'row-2',
@@ -252,8 +252,8 @@ describe('InviteTeamMembers', () => {
{ email: 'user2@test.com', success: false, error: 'Error 2' },
];
const mockRows: InviteMemberRow[] = [
{ id: 'row-1', email: 'user1@test.com', roleIds: ['role-editor-id'] },
{ id: 'row-2', email: 'user2@test.com', roleIds: ['role-viewer-id'] },
{ id: 'row-1', email: 'user1@test.com', roleId: 'role-editor-id' },
{ id: 'row-2', email: 'user2@test.com', roleId: 'role-viewer-id' },
];
mockInviteMembersProps?.onAllFailed?.(mockResults, mockRows);
@@ -263,14 +263,14 @@ describe('InviteTeamMembers', () => {
teamMembers: [
{
email: 'user1@test.com',
roles: ['EDITOR'],
role: 'EDITOR',
name: '',
frontendBaseUrl: 'http://localhost:3301',
id: 'row-1',
},
{
email: 'user2@test.com',
roles: ['VIEWER'],
role: 'VIEWER',
name: '',
frontendBaseUrl: 'http://localhost:3301',
id: 'row-2',

View File

@@ -1,4 +1,4 @@
import { MouseEvent } from 'react';
import { MouseEventHandler } from 'react';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
@@ -11,7 +11,7 @@ export type UseCopyLogLink = {
isHighlighted: boolean;
isLogsExplorerPage: boolean;
activeLogId: string | null;
onLogCopy: (event?: MouseEvent<HTMLElement>) => void;
onLogCopy: MouseEventHandler<HTMLElement>;
onClearActiveLog: () => void;
};

View File

@@ -1,4 +1,10 @@
import { MouseEvent, useCallback, useEffect, useMemo, useState } from 'react';
import {
MouseEventHandler,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
@@ -40,14 +46,14 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
[pathname],
);
const onLogCopy = useCallback(
(event?: MouseEvent<HTMLElement>): void => {
const onLogCopy: MouseEventHandler<HTMLElement> = useCallback(
(event) => {
if (!logId) {
return;
}
event?.preventDefault();
event?.stopPropagation();
event.preventDefault();
event.stopPropagation();
urlQuery.delete(QueryParams.activeLogId);
urlQuery.delete(QueryParams.relativeTime);
@@ -60,7 +66,7 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
setCopy(link);
toast.success('Copied to clipboard', { position: 'bottom-right' });
toast.success('Copied to clipboard', { position: 'top-right' });
},
[logId, urlQuery, minTime, maxTime, pathname, setCopy],
);

View File

@@ -139,6 +139,27 @@ export const handlers = [
return res(ctx.status(500));
},
),
rest.get('http://localhost/api/v1/loginPrecheck', (req, res, ctx) => {
const email = req.url.searchParams.get('email');
if (email === 'failEmail@signoz.io') {
return res(ctx.status(500));
}
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
sso: true,
ssoUrl: '',
canSelfRegister: false,
isUser: true,
ssoError: '',
},
}),
);
}),
rest.get('http://localhost/api/v2/licenses', (req, res, ctx) =>
res(ctx.status(200), ctx.json(licensesSuccessResponse)),
),
@@ -173,6 +194,14 @@ export const handlers = [
}),
),
),
rest.put('http://localhost/api/v1/user/:id', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
data: 'user updated successfully',
}),
),
),
rest.get(
'http://localhost/api/v3/autocomplete/aggregate_attributes',
(req, res, ctx) =>

View File

@@ -0,0 +1,12 @@
import { User } from 'types/reducer/app';
export interface UserProps {
name: User['displayName'];
email: User['email'];
role: string;
frontendBaseUrl: string;
}
export interface UsersProps {
invites: UserProps[];
}

2
go.mod
View File

@@ -4,7 +4,7 @@ go 1.25.7
require (
dario.cat/mergo v1.0.2
github.com/AfterShip/clickhouse-sql-parser v0.5.5
github.com/AfterShip/clickhouse-sql-parser v0.5.4
github.com/ClickHouse/clickhouse-go/v2 v2.44.0
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/clickhouse-go-mock v0.14.0

4
go.sum
View File

@@ -66,8 +66,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/AfterShip/clickhouse-sql-parser v0.5.5 h1:LCA23yAA4GgF73PoYXb67yzCdC4sXsj4geQz1Oij3U8=
github.com/AfterShip/clickhouse-sql-parser v0.5.5/go.mod h1:Qi3qvPTfZb/aFwI5V4WFOahgjsLJa4MzVijIAfwOhDw=
github.com/AfterShip/clickhouse-sql-parser v0.5.4 h1:yiCQaMq8EO+dpKdnpP9YYd/ne6MSuOXgsMsNL33NiTI=
github.com/AfterShip/clickhouse-sql-parser v0.5.4/go.mod h1:Qi3qvPTfZb/aFwI5V4WFOahgjsLJa4MzVijIAfwOhDw=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=

View File

@@ -25,7 +25,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/savedview"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
@@ -76,7 +75,6 @@ type provider struct {
rulerHandler ruler.Handler
llmPricingRuleHandler llmpricingrule.Handler
statsHandler statsreporter.Handler
savedViewHandler savedview.Handler
}
func NewFactory(
@@ -112,7 +110,6 @@ func NewFactory(
traceDetailHandler tracedetail.Handler,
rulerHandler ruler.Handler,
statsHandler statsreporter.Handler,
savedViewHandler savedview.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
return newProvider(
@@ -151,7 +148,6 @@ func NewFactory(
traceDetailHandler,
rulerHandler,
statsHandler,
savedViewHandler,
)
})
}
@@ -192,7 +188,6 @@ func newProvider(
traceDetailHandler tracedetail.Handler,
rulerHandler ruler.Handler,
statsHandler statsreporter.Handler,
savedViewHandler savedview.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
@@ -232,7 +227,6 @@ func newProvider(
rulerHandler: rulerHandler,
llmPricingRuleHandler: llmPricingRuleHandler,
statsHandler: statsHandler,
savedViewHandler: savedViewHandler,
}
provider.authzMiddleware = middleware.NewAuthZ(settings.Logger(), orgGetter, authzService)
@@ -365,10 +359,6 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addSavedViewRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -1,151 +0,0 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/gorilla/mux"
)
func (provider *provider) addSavedViewRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/saved_views", handler.New(
provider.authzMiddleware.CheckResources(provider.savedViewHandler.ListV2, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "ListSavedViews",
Tags: []string{"saved_view"},
Summary: "List saved views",
Description: "Returns saved views, optionally filtered by source and name.",
Request: nil,
RequestQuery: new(savedviewtypes.ListSavedViewsParams),
RequestContentType: "",
Response: new([]*savedviewtypes.SavedView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceSavedView.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceSavedView,
Verb: coretypes.VerbList,
Category: coretypes.ActionCategoryDataAccess,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/saved_views", handler.New(
provider.authzMiddleware.CheckResources(provider.savedViewHandler.CreateV2, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName),
handler.OpenAPIDef{
ID: "CreateSavedView",
Tags: []string{"saved_view"},
Summary: "Create saved view",
Description: "Persists a saved view for the explore page. Returns the id of the created view.",
Request: new(savedviewtypes.PostableSavedView),
RequestContentType: "application/json",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceSavedView.Scope(coretypes.VerbCreate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceSavedView,
Verb: coretypes.VerbCreate,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.ResponseJSONPath("data.id"),
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/saved_views/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.savedViewHandler.GetV2, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "GetSavedView",
Tags: []string{"saved_view"},
Summary: "Get saved view",
Description: "Returns a saved view by id.",
Request: nil,
RequestContentType: "",
Response: new(savedviewtypes.SavedView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceSavedView.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceSavedView,
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/saved_views/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.savedViewHandler.UpdateV2, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName),
handler.OpenAPIDef{
ID: "UpdateSavedView",
Tags: []string{"saved_view"},
Summary: "Update saved view",
Description: "Replaces a saved view's name and query.",
Request: new(savedviewtypes.UpdatableSavedView),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceSavedView.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceSavedView,
Verb: coretypes.VerbUpdate,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/saved_views/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.savedViewHandler.Delete, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName),
handler.OpenAPIDef{
ID: "DeleteSavedView",
Tags: []string{"saved_view"},
Summary: "Delete saved view",
Description: "Deletes a saved view by id.",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceSavedView.Scope(coretypes.VerbDelete)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceSavedView,
Verb: coretypes.VerbDelete,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -27,6 +27,22 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/invite/bulk", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.CreateBulkInvite), handler.OpenAPIDef{
ID: "CreateBulkInvite",
Tags: []string{"users"},
Summary: "Create bulk invite",
Description: "This endpoint creates a bulk invite for a user",
Request: new(types.PostableBulkInviteRequest),
RequestContentType: "application/json",
Response: nil,
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.ListUsersDeprecated), handler.OpenAPIDef{
ID: "ListUsersDeprecated",
Tags: []string{"users"},
@@ -122,13 +138,30 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: authtypes.IdentNProviderTokenizer.StringValue()}},
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authzMiddleware.SelfAccess(provider.userHandler.GetUserDeprecated), handler.OpenAPIDef{
ID: "GetUserDeprecated",
Tags: []string{"users"},
Summary: "Get user",
Description: "This endpoint returns the user by id",
Request: nil,
RequestContentType: "",
Response: new(types.DeprecatedUser),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.GetUser), handler.OpenAPIDef{
ID: "GetUser",
Tags: []string{"users"},
@@ -146,6 +179,23 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authzMiddleware.SelfAccess(provider.userHandler.UpdateUserDeprecated), handler.OpenAPIDef{
ID: "UpdateUserDeprecated",
Tags: []string{"users"},
Summary: "Update user",
Description: "This endpoint updates the user by id",
Request: new(types.DeprecatedUser),
RequestContentType: "application/json",
Response: new(types.DeprecatedUser),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.UpdateUser), handler.OpenAPIDef{
ID: "UpdateUser",
Tags: []string{"users"},
@@ -163,6 +213,23 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.DeleteUser), handler.OpenAPIDef{
ID: "DeleteUserDeprecated",
Tags: []string{"users"},
Summary: "Delete user",
Description: "This endpoint deletes the user by id",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.DeleteUser), handler.OpenAPIDef{
ID: "DeleteUser",
Tags: []string{"users"},
@@ -173,7 +240,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
@@ -249,7 +316,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/resetPassword", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.ResetPassword), handler.OpenAPIDef{
ID: "ResetPasswordDeprecated",
ID: "ResetPassword",
Tags: []string{"users"},
Summary: "Reset password",
Description: "This endpoint resets the password by token",
@@ -259,7 +326,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: true,
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
return err
@@ -299,23 +366,6 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/factor_password/reset", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.ResetPassword), handler.OpenAPIDef{
ID: "ResetPassword",
Tags: []string{"users"},
Summary: "Reset password",
Description: "This endpoint resets the password using a single use reset password token",
Request: new(types.PostableResetPassword),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/users/{id}/roles", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.GetRolesByUserID), handler.OpenAPIDef{
ID: "GetRolesByUserID",
Tags: []string{"users"},
@@ -343,7 +393,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
@@ -360,7 +410,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {

View File

@@ -14,6 +14,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
const (
@@ -150,6 +151,40 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc {
})
}
func (middleware *AuthZ) SelfAccess(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil {
render.Error(rw, err)
return
}
selectors := []coretypes.Selector{
coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(
req.Context(),
claims,
valuer.MustNewUUID(claims.OrgID),
authtypes.Relation{Verb: coretypes.VerbAssignee},
coretypes.NewResourceRole(),
selectors,
selectors,
)
if err != nil {
id := mux.Vars(req)["id"]
if err := claims.IsSelfAccess(id); err != nil {
middleware.logger.WarnContext(req.Context(), authzDeniedMessage, slog.Any("claims", claims))
render.Error(rw, err)
return
}
}
next(rw, req)
})
}
func (middleware *AuthZ) OpenAccess(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
next(rw, req)

View File

@@ -5,7 +5,7 @@
"overview": "file://overview.md",
"supportedSignals": {
"metrics": true,
"logs": false
"logs": true
},
"dataCollected": {
"metrics": [

View File

@@ -5,7 +5,7 @@
"overview": "file://overview.md",
"supportedSignals": {
"metrics": true,
"logs": false
"logs": true
},
"dataCollected": {
"metrics": [

View File

@@ -5,7 +5,7 @@
"overview": "file://overview.md",
"supportedSignals": {
"metrics": true,
"logs": false
"logs": true
},
"dataCollected": {
"metrics": [

View File

@@ -5,7 +5,7 @@
"overview": "file://overview.md",
"supportedSignals": {
"metrics": true,
"logs": false
"logs": true
},
"dataCollected": {
"metrics": [

View File

@@ -5,7 +5,7 @@
"overview": "file://overview.md",
"supportedSignals": {
"metrics": true,
"logs": false
"logs": true
},
"dataCollected": {
"metrics": [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,8 +11,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/savedview"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -25,116 +23,6 @@ func NewHandler(module savedview.Module) savedview.Handler {
return &handler{module: module}
}
// legacyExtraData mirrors the frontend's extraData JSON shape so /api/v1
// responses can synthesize the same shape back for the legacy frontend.
type legacyExtraData struct {
Color string `json:"color,omitempty"`
SelectColumns []telemetrytypes.TelemetryFieldKey `json:"selectColumns,omitempty"`
Format string `json:"format,omitempty"`
MaxLines int `json:"maxLines,omitempty"`
FontSize string `json:"fontSize,omitempty"`
}
// newPostableSavedViewFromLegacyView builds a create payload for a v1 request.
func newPostableSavedViewFromLegacyView(v *v3.SavedView) savedviewtypes.PostableSavedView {
var legacy legacyExtraData
if v.ExtraData != "" {
// Best-effort: malformed/older extraData shapes never fail the request
_ = json.Unmarshal([]byte(v.ExtraData), &legacy)
}
return savedviewtypes.PostableSavedView{
GenerateName: true,
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
Data: savedviewtypes.SavedViewData{
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: v.Name,
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
Queries: v.CompositeQuery.Queries,
SelectedFields: legacy.SelectColumns,
Display: savedviewtypes.Display{
MaxLines: legacy.MaxLines,
FontSize: legacy.FontSize,
Format: legacy.Format,
Color: legacy.Color,
},
},
},
}
}
// newUpdatableSavedViewFromLegacyView builds an update payload for a v1 request.
func newUpdatableSavedViewFromLegacyView(v *v3.SavedView) savedviewtypes.UpdatableSavedView {
var legacy legacyExtraData
if v.ExtraData != "" {
// Best-effort: malformed/older extraData shapes never fail the request
_ = json.Unmarshal([]byte(v.ExtraData), &legacy)
}
return savedviewtypes.UpdatableSavedView{
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
Data: savedviewtypes.SavedViewData{
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: v.Name,
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
Queries: v.CompositeQuery.Queries,
SelectedFields: legacy.SelectColumns,
Display: savedviewtypes.Display{
MaxLines: legacy.MaxLines,
FontSize: legacy.FontSize,
Format: legacy.Format,
Color: legacy.Color,
},
},
},
}
}
// newLegacyViewFromSavedView renders a v2 SavedView back into the v1 shape.
func newLegacyViewFromSavedView(v *savedviewtypes.SavedView) (*v3.SavedView, error) {
extraData, err := json.Marshal(legacyExtraData{
Color: v.Data.Spec.Display.Color,
SelectColumns: v.Data.Spec.SelectedFields,
Format: v.Data.Spec.Display.Format,
MaxLines: v.Data.Spec.Display.MaxLines,
FontSize: v.Data.Spec.Display.FontSize,
})
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in marshalling extra data")
}
return &v3.SavedView{
ID: v.ID,
Name: v.Data.Spec.DisplayName,
CreatedAt: v.CreatedAt,
CreatedBy: v.CreatedBy,
UpdatedAt: v.UpdatedAt,
UpdatedBy: v.UpdatedBy,
SourcePage: v.Source.StringValue(),
CompositeQuery: &v3.CompositeQuery{
PanelType: v3.PanelType(v.Data.Spec.PanelType.StringValue()),
// Saved views are only ever created from the explorer's builder mode.
QueryType: v3.QueryTypeBuilder,
Queries: v.Data.Spec.Queries,
},
ExtraData: string(extraData),
}, nil
}
func newLegacyViewsFromSavedViews(views []*savedviewtypes.SavedView) ([]*v3.SavedView, error) {
out := make([]*v3.SavedView, 0, len(views))
for _, view := range views {
legacyView, err := newLegacyViewFromSavedView(view)
if err != nil {
return nil, err
}
out = append(out, legacyView)
}
return out, nil
}
func (handler *handler) Create(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -156,7 +44,7 @@ func (handler *handler) Create(w http.ResponseWriter, r *http.Request) {
return
}
uuid, err := handler.module.CreateView(ctx, claims.OrgID, newPostableSavedViewFromLegacyView(&view))
uuid, err := handler.module.CreateView(ctx, claims.OrgID, view)
if err != nil {
render.Error(w, err)
return
@@ -175,7 +63,7 @@ func (handler *handler) Get(w http.ResponseWriter, r *http.Request) {
return
}
viewID := mux.Vars(r)["id"]
viewID := mux.Vars(r)["viewId"]
viewUUID, err := valuer.NewUUID(viewID)
if err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse view id"))
@@ -188,13 +76,7 @@ func (handler *handler) Get(w http.ResponseWriter, r *http.Request) {
return
}
legacyView, err := newLegacyViewFromSavedView(view)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, legacyView)
render.Success(w, http.StatusOK, view)
}
func (handler *handler) Update(w http.ResponseWriter, r *http.Request) {
@@ -207,7 +89,7 @@ func (handler *handler) Update(w http.ResponseWriter, r *http.Request) {
return
}
viewID := mux.Vars(r)["id"]
viewID := mux.Vars(r)["viewId"]
viewUUID, err := valuer.NewUUID(viewID)
if err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse view id"))
@@ -224,7 +106,7 @@ func (handler *handler) Update(w http.ResponseWriter, r *http.Request) {
return
}
err = handler.module.UpdateView(ctx, claims.OrgID, viewUUID, newUpdatableSavedViewFromLegacyView(&view))
err = handler.module.UpdateView(ctx, claims.OrgID, viewUUID, view)
if err != nil {
render.Error(w, err)
return
@@ -243,7 +125,7 @@ func (handler *handler) Delete(w http.ResponseWriter, r *http.Request) {
return
}
viewID := mux.Vars(r)["id"]
viewID := mux.Vars(r)["viewId"]
viewUUID, err := valuer.NewUUID(viewID)
if err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse view id"))
@@ -256,7 +138,7 @@ func (handler *handler) Delete(w http.ResponseWriter, r *http.Request) {
return
}
render.Success(w, http.StatusNoContent, nil)
render.Success(w, http.StatusOK, nil)
}
func (handler *handler) List(w http.ResponseWriter, r *http.Request) {
@@ -271,18 +153,13 @@ func (handler *handler) List(w http.ResponseWriter, r *http.Request) {
sourcePage := r.URL.Query().Get("sourcePage")
name := r.URL.Query().Get("name")
category := r.URL.Query().Get("category")
views, err := handler.module.GetViewsForFilters(r.Context(), claims.OrgID, savedviewtypes.Source{String: valuer.NewString(sourcePage)}, name)
queries, err := handler.module.GetViewsForFilters(r.Context(), claims.OrgID, sourcePage, name, category)
if err != nil {
render.Error(w, err)
return
}
legacyViews, err := newLegacyViewsFromSavedViews(views)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, legacyViews)
render.Success(w, http.StatusOK, queries)
}

View File

@@ -1,197 +0,0 @@
package implsavedview
import (
"encoding/json"
"testing"
"time"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testQueries() []qbtypes.QueryEnvelope {
return []qbtypes.QueryEnvelope{
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Aggregations: []qbtypes.LogAggregation{{Expression: "count()"}},
},
},
}
}
func TestNewPostableSavedViewFromLegacyView(t *testing.T) {
t.Run("all fields carried over", func(t *testing.T) {
legacy := &v3.SavedView{
Name: "my view",
SourcePage: "logs",
CompositeQuery: &v3.CompositeQuery{
PanelType: v3.PanelTypeGraph,
Queries: testQueries(),
},
ExtraData: `{"color":"blue","selectColumns":[{"name":"service.name"}],"format":"table","maxLines":10,"fontSize":"large"}`,
}
postable := newPostableSavedViewFromLegacyView(legacy)
assert.Empty(t, postable.Name, "v1 has no slug concept -- name must always be generated")
assert.True(t, postable.GenerateName, "v1 has no slug concept -- name must always be generated")
assert.Equal(t, "my view", postable.Data.Spec.DisplayName)
assert.Equal(t, savedviewtypes.SourceLogs, postable.Source)
assert.Equal(t, savedviewtypes.SavedViewSchemaVersion, postable.Data.SchemaVersion)
assert.Equal(t, savedviewtypes.PanelTypeGraph, postable.Data.Spec.PanelType)
assert.Equal(t, legacy.CompositeQuery.Queries, postable.Data.Spec.Queries)
assert.Equal(t, []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}}, postable.Data.Spec.SelectedFields)
assert.Equal(t, savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"}, postable.Data.Spec.Display)
})
t.Run("empty extra data leaves display and selected fields zero-valued", func(t *testing.T) {
legacy := &v3.SavedView{
Name: "no extra data",
SourcePage: "traces",
CompositeQuery: &v3.CompositeQuery{
PanelType: v3.PanelTypeTable,
Queries: testQueries(),
},
ExtraData: "",
}
postable := newPostableSavedViewFromLegacyView(legacy)
assert.Equal(t, savedviewtypes.Display{}, postable.Data.Spec.Display)
assert.Nil(t, postable.Data.Spec.SelectedFields)
})
t.Run("malformed extra data is ignored, not an error", func(t *testing.T) {
legacy := &v3.SavedView{
Name: "malformed extra data",
SourcePage: "metrics",
CompositeQuery: &v3.CompositeQuery{
PanelType: v3.PanelTypeList,
Queries: testQueries(),
},
ExtraData: `{not valid json`,
}
postable := newPostableSavedViewFromLegacyView(legacy)
assert.Equal(t, "malformed extra data", postable.Data.Spec.DisplayName)
assert.Equal(t, savedviewtypes.Display{}, postable.Data.Spec.Display)
})
}
func TestNewUpdatableSavedViewFromLegacyView(t *testing.T) {
legacy := &v3.SavedView{
Name: "renamed view",
SourcePage: "traces",
CompositeQuery: &v3.CompositeQuery{
PanelType: v3.PanelTypeTable,
Queries: testQueries(),
},
ExtraData: `{"color":"red"}`,
}
updatable := newUpdatableSavedViewFromLegacyView(legacy)
assert.Equal(t, "renamed view", updatable.Data.Spec.DisplayName)
assert.Equal(t, savedviewtypes.SourceTraces, updatable.Source)
}
func TestNewLegacyViewFromSavedView(t *testing.T) {
now := time.Now()
savedView := &savedviewtypes.SavedView{
Name: "my-view-abc123ef",
Source: savedviewtypes.SourceLogs,
Data: savedviewtypes.SavedViewData{
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: "my view",
PanelType: savedviewtypes.PanelTypeGraph,
Queries: testQueries(),
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
Display: savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"},
},
},
}
savedView.ID = valuer.GenerateUUID()
savedView.CreatedAt = now
savedView.UpdatedAt = now
savedView.CreatedBy = "creator@signoz.io"
savedView.UpdatedBy = "updater@signoz.io"
legacy, err := newLegacyViewFromSavedView(savedView)
require.NoError(t, err)
assert.Equal(t, savedView.ID, legacy.ID)
assert.Equal(t, savedView.Data.Spec.DisplayName, legacy.Name)
assert.Equal(t, savedView.CreatedAt, legacy.CreatedAt)
assert.Equal(t, savedView.CreatedBy, legacy.CreatedBy)
assert.Equal(t, savedView.UpdatedAt, legacy.UpdatedAt)
assert.Equal(t, savedView.UpdatedBy, legacy.UpdatedBy)
assert.Equal(t, "logs", legacy.SourcePage)
assert.Equal(t, v3.PanelTypeGraph, legacy.CompositeQuery.PanelType)
assert.Equal(t, v3.QueryTypeBuilder, legacy.CompositeQuery.QueryType)
assert.Equal(t, savedView.Data.Spec.Queries, legacy.CompositeQuery.Queries)
var extra legacyExtraData
require.NoError(t, json.Unmarshal([]byte(legacy.ExtraData), &extra))
assert.Equal(t, "blue", extra.Color)
assert.Equal(t, savedView.Data.Spec.SelectedFields, extra.SelectColumns)
assert.Equal(t, "table", extra.Format)
assert.Equal(t, 10, extra.MaxLines)
assert.Equal(t, "large", extra.FontSize)
}
func TestNewLegacyViewsFromSavedViews(t *testing.T) {
a := &savedviewtypes.SavedView{Name: "a-slug", Source: savedviewtypes.SourceLogs, Data: savedviewtypes.SavedViewData{Spec: savedviewtypes.SavedViewSpec{DisplayName: "a", PanelType: savedviewtypes.PanelTypeGraph, Queries: testQueries()}}}
b := &savedviewtypes.SavedView{Name: "b-slug", Source: savedviewtypes.SourceTraces, Data: savedviewtypes.SavedViewData{Spec: savedviewtypes.SavedViewSpec{DisplayName: "b", PanelType: savedviewtypes.PanelTypeTable, Queries: testQueries()}}}
legacyViews, err := newLegacyViewsFromSavedViews([]*savedviewtypes.SavedView{a, b})
require.NoError(t, err)
require.Len(t, legacyViews, 2)
assert.Equal(t, "a", legacyViews[0].Name)
assert.Equal(t, "b", legacyViews[1].Name)
}
// TestLegacyViewRoundTrip guards the whole v1<->v2 bridge: converting a
// SavedView to its legacy shape and back must recover the fields the legacy
// frontend round-trips through (displayName, source, panelType, queries,
// selectedFields, display) -- these two functions are each other's inverse
// on the API surface, so a regression in either should fail this. The internal
// slug (Name) is deliberately NOT part of this contract -- v1 never sees it.
func TestLegacyViewRoundTrip(t *testing.T) {
original := &savedviewtypes.SavedView{
Name: "round-trip-abc123ef",
Source: savedviewtypes.SourceMetrics,
Data: savedviewtypes.SavedViewData{
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: "round trip",
PanelType: savedviewtypes.PanelTypeTable,
Queries: testQueries(),
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
Display: savedviewtypes.Display{MaxLines: 5, FontSize: "small", Format: "list", Color: "red"},
},
},
}
legacy, err := newLegacyViewFromSavedView(original)
require.NoError(t, err)
roundTripped := newPostableSavedViewFromLegacyView(legacy)
assert.Empty(t, roundTripped.Name)
assert.True(t, roundTripped.GenerateName)
assert.Equal(t, original.Data.Spec.DisplayName, roundTripped.Data.Spec.DisplayName)
assert.Equal(t, original.Source, roundTripped.Source)
assert.Equal(t, original.Data.Spec.PanelType, roundTripped.Data.Spec.PanelType)
assert.Equal(t, original.Data.Spec.Queries, roundTripped.Data.Spec.Queries)
assert.Equal(t, original.Data.Spec.SelectedFields, roundTripped.Data.Spec.SelectedFields)
assert.Equal(t, original.Data.Spec.Display, roundTripped.Data.Spec.Display)
}

View File

@@ -1,135 +0,0 @@
package implsavedview
import (
"context"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
func (handler *handler) CreateV2(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
var view savedviewtypes.PostableSavedView
if err := binding.JSON.BindBody(r.Body, &view, binding.WithDisallowUnknownFields(true)); err != nil {
render.Error(w, err)
return
}
if err := view.Validate(); err != nil {
render.Error(w, err)
return
}
uuid, err := handler.module.CreateView(ctx, claims.OrgID, view)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusCreated, types.Identifiable{ID: uuid})
}
func (handler *handler) GetV2(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
viewID := mux.Vars(r)["id"]
viewUUID, err := valuer.NewUUID(viewID)
if err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse view id"))
return
}
view, err := handler.module.GetView(ctx, claims.OrgID, viewUUID)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, view)
}
func (handler *handler) UpdateV2(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
viewID := mux.Vars(r)["id"]
viewUUID, err := valuer.NewUUID(viewID)
if err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse view id"))
return
}
var view savedviewtypes.UpdatableSavedView
if err := binding.JSON.BindBody(r.Body, &view, binding.WithDisallowUnknownFields(true)); err != nil {
render.Error(w, err)
return
}
if err := view.Validate(); err != nil {
render.Error(w, err)
return
}
err = handler.module.UpdateView(ctx, claims.OrgID, viewUUID, view)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusNoContent, nil)
}
func (handler *handler) ListV2(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
params := new(savedviewtypes.ListSavedViewsParams)
if err := binding.Query.BindQuery(r.URL.Query(), params); err != nil {
render.Error(w, err)
return
}
if err := params.Validate(); err != nil {
render.Error(w, err)
return
}
queries, err := handler.module.GetViewsForFilters(r.Context(), claims.OrgID, params.Source, params.Name)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, queries)
}

View File

@@ -2,59 +2,185 @@ package implsavedview
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/savedview"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct {
store savedviewtypes.Store
sqlstore sqlstore.SQLStore
}
func NewModule(store savedviewtypes.Store) savedview.Module {
return &module{store: store}
func NewModule(sqlstore sqlstore.SQLStore) savedview.Module {
return &module{sqlstore: sqlstore}
}
func (module *module) GetViewsForFilters(ctx context.Context, orgID string, source savedviewtypes.Source, name string) ([]*savedviewtypes.SavedView, error) {
return module.store.List(ctx, orgID, source, name)
}
func (module *module) CreateView(ctx context.Context, orgID string, view savedviewtypes.PostableSavedView) (valuer.UUID, error) {
claims, err := authtypes.ClaimsFromContext(ctx)
func (module *module) GetViewsForFilters(ctx context.Context, orgID string, sourcePage string, name string, category string) ([]*v3.SavedView, error) {
var views []savedviewtypes.SavedView
var err error
if len(category) == 0 {
err = module.sqlstore.BunDB().NewSelect().Model(&views).Where("org_id = ? AND source_page = ? AND name LIKE ?", orgID, sourcePage, "%"+name+"%").Scan(ctx)
} else {
err = module.sqlstore.BunDB().NewSelect().Model(&views).Where("org_id = ? AND source_page = ? AND category LIKE ? AND name LIKE ?", orgID, sourcePage, "%"+category+"%", "%"+name+"%").Scan(ctx)
}
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in getting saved views")
}
var savedViews []*v3.SavedView
for _, view := range views {
var compositeQuery v3.CompositeQuery
err = json.Unmarshal([]byte(view.Data), &compositeQuery)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in unmarshalling explorer query data: %s", err.Error())
}
savedViews = append(savedViews, &v3.SavedView{
ID: view.ID,
Name: view.Name,
CreatedAt: view.CreatedAt,
CreatedBy: view.CreatedBy,
UpdatedAt: view.UpdatedAt,
UpdatedBy: view.UpdatedBy,
Tags: strings.Split(view.Tags, ","),
SourcePage: view.SourcePage,
CompositeQuery: &compositeQuery,
ExtraData: view.ExtraData,
})
}
return savedViews, nil
}
func (module *module) CreateView(ctx context.Context, orgID string, view v3.SavedView) (valuer.UUID, error) {
data, err := json.Marshal(view.CompositeQuery)
if err != nil {
return valuer.UUID{}, errors.WrapInternalf(err, errors.CodeInternal, "error in marshalling explorer query data")
}
uuid := valuer.GenerateUUID()
createdAt := time.Now()
updatedAt := time.Now()
claims, errv2 := authtypes.ClaimsFromContext(ctx)
if errv2 != nil {
return valuer.UUID{}, errors.NewInternalf(errors.CodeInternal, "error in getting email from context")
}
dbView := view.ToSavedView(orgID, claims.Email)
createBy := claims.Email
updatedBy := claims.Email
if err := module.store.Create(ctx, dbView); err != nil {
return valuer.UUID{}, err
dbView := savedviewtypes.SavedView{
TimeAuditable: types.TimeAuditable{
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
UserAuditable: types.UserAuditable{
CreatedBy: createBy,
UpdatedBy: updatedBy,
},
OrgID: orgID,
Identifiable: types.Identifiable{
ID: uuid,
},
Name: view.Name,
Category: view.Category,
SourcePage: view.SourcePage,
Tags: strings.Join(view.Tags, ","),
Data: string(data),
ExtraData: view.ExtraData,
}
return dbView.ID, nil
}
func (module *module) GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*savedviewtypes.SavedView, error) {
return module.store.Get(ctx, orgID, uuid)
}
func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.UUID, view savedviewtypes.UpdatableSavedView) error {
claims, err := authtypes.ClaimsFromContext(ctx)
_, err = module.sqlstore.BunDB().NewInsert().Model(&dbView).Exec(ctx)
if err != nil {
return valuer.UUID{}, errors.WrapInternalf(err, errors.CodeInternal, "error in creating saved view")
}
return uuid, nil
}
func (module *module) GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*v3.SavedView, error) {
var view savedviewtypes.SavedView
err := module.sqlstore.BunDB().NewSelect().Model(&view).Where("org_id = ? AND id = ?", orgID, uuid.StringValue()).Scan(ctx)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in getting saved view")
}
var compositeQuery v3.CompositeQuery
err = json.Unmarshal([]byte(view.Data), &compositeQuery)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in unmarshalling explorer query data")
}
return &v3.SavedView{
ID: view.ID,
Name: view.Name,
Category: view.Category,
CreatedAt: view.CreatedAt,
CreatedBy: view.CreatedBy,
UpdatedAt: view.UpdatedAt,
UpdatedBy: view.UpdatedBy,
SourcePage: view.SourcePage,
Tags: strings.Split(view.Tags, ","),
CompositeQuery: &compositeQuery,
ExtraData: view.ExtraData,
}, nil
}
func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.UUID, view v3.SavedView) error {
data, err := json.Marshal(view.CompositeQuery)
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "error in marshalling explorer query data")
}
claims, errv2 := authtypes.ClaimsFromContext(ctx)
if errv2 != nil {
return errors.NewInternalf(errors.CodeInternal, "error in getting email from context")
}
return module.store.Update(ctx, view.ToSavedView(uuid, orgID, claims.Email))
updatedAt := time.Now()
updatedBy := claims.Email
_, err = module.sqlstore.BunDB().NewUpdate().
Model(&savedviewtypes.SavedView{}).
Set("updated_at = ?, updated_by = ?, name = ?, category = ?, source_page = ?, tags = ?, data = ?, extra_data = ?",
updatedAt, updatedBy, view.Name, view.Category, view.SourcePage, strings.Join(view.Tags, ","), data, view.ExtraData).
Where("id = ?", uuid.StringValue()).
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "error in updating saved view")
}
return nil
}
func (module *module) DeleteView(ctx context.Context, orgID string, uuid valuer.UUID) error {
return module.store.Delete(ctx, orgID, uuid)
_, err := module.sqlstore.BunDB().NewDelete().
Model(&savedviewtypes.SavedView{}).
Where("id = ?", uuid.StringValue()).
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "error in deleting explorer query")
}
return nil
}
func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
savedViews, err := module.store.List(ctx, orgID.StringValue(), savedviewtypes.Source{}, "")
savedViews := []*savedviewtypes.SavedView{}
err := module.
sqlstore.
BunDB().
NewSelect().
Model(&savedViews).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, err
}

View File

@@ -1,299 +0,0 @@
package implsavedview_test
import (
"context"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/savedview"
"github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest"
"github.com/SigNoz/signoz/pkg/types/authtypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes/savedviewtypestest"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestStore() (savedview.Module, *savedviewtypestest.StoreTest) {
sqlStore := sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherRegexp)
store := implsavedview.NewStore(sqlStore)
return implsavedview.NewModule(store), savedviewtypestest.New(store, sqlStore.Mock())
}
func testPostableSavedView(name string, source savedviewtypes.Source) savedviewtypes.PostableSavedView {
return savedviewtypes.PostableSavedView{
Name: name,
Source: source,
Data: savedviewtypes.SavedViewData{
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
DisplayName: name,
PanelType: savedviewtypes.PanelTypeGraph,
Queries: []qbtypes.QueryEnvelope{
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Aggregations: []qbtypes.LogAggregation{{Expression: "count()"}},
},
},
},
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
},
},
}
}
func testUpdatableSavedView(displayName string, source savedviewtypes.Source) savedviewtypes.UpdatableSavedView {
postable := testPostableSavedView(displayName, source)
return savedviewtypes.UpdatableSavedView{
Source: postable.Source,
Data: postable.Data,
}
}
func testSavedView(orgID string, id valuer.UUID, updatedBy string, view savedviewtypes.PostableSavedView) *savedviewtypes.SavedView {
savedView := view.ToSavedView(orgID, "creator@signoz.io")
savedView.ID = id
savedView.UpdatedBy = updatedBy
return savedView
}
func contextWithClaims(orgID, email string) context.Context {
return authtypes.NewContextWithClaims(context.Background(), authtypes.Claims{
OrgID: orgID,
Email: email,
})
}
func TestModule_CreateAndGetView(t *testing.T) {
m, st := newTestStore()
orgID := valuer.GenerateUUID().StringValue()
ctx := contextWithClaims(orgID, "creator@signoz.io")
view := testPostableSavedView("my view", savedviewtypes.SourceLogs)
st.ExpectCreate()
id, err := m.CreateView(ctx, orgID, view)
require.NoError(t, err)
require.False(t, id.IsZero())
stored := testSavedView(orgID, id, "creator@signoz.io", view)
st.ExpectGet(orgID, id, stored)
got, err := m.GetView(ctx, orgID, id)
require.NoError(t, err)
assert.Equal(t, id, got.ID)
assert.Equal(t, "my view", got.Name)
assert.Equal(t, savedviewtypes.SourceLogs, got.Source)
assert.Equal(t, "creator@signoz.io", got.CreatedBy)
assert.Equal(t, "creator@signoz.io", got.UpdatedBy)
assert.Equal(t, savedviewtypes.PanelTypeGraph, got.Data.Spec.PanelType)
require.NoError(t, st.AssertExpectations())
}
func TestModule_GetView_NotFound(t *testing.T) {
m, st := newTestStore()
orgID := valuer.GenerateUUID().StringValue()
id := valuer.GenerateUUID()
st.ExpectGet(orgID, id, nil)
_, err := m.GetView(contextWithClaims(orgID, "someone@signoz.io"), orgID, id)
require.Error(t, err)
assert.True(t, errors.Ast(err, errors.TypeNotFound), "expected a not-found error, got %v", err)
require.NoError(t, st.AssertExpectations())
}
func TestModule_GetView_ScopedToOrg(t *testing.T) {
m, st := newTestStore()
orgB := valuer.GenerateUUID().StringValue()
id := valuer.GenerateUUID()
// The mock only has an expectation for orgB's WHERE clause; a lookup
// scoped to org A's real id must not accidentally match it.
st.ExpectGet(orgB, id, nil)
_, err := m.GetView(contextWithClaims(orgB, "b@signoz.io"), orgB, id)
require.Error(t, err, "a view created under org A must not be visible to org B")
assert.True(t, errors.Ast(err, errors.TypeNotFound), "expected a not-found error, got %v", err)
require.NoError(t, st.AssertExpectations())
}
func TestModule_UpdateView(t *testing.T) {
m, st := newTestStore()
orgID := valuer.GenerateUUID().StringValue()
id := valuer.GenerateUUID()
existing := testSavedView(orgID, id, "creator@signoz.io", testPostableSavedView("my-view", savedviewtypes.SourceLogs))
existingName := existing.Name
updated := testUpdatableSavedView("renamed", savedviewtypes.SourceTraces)
updated.Data.Spec.PanelType = savedviewtypes.PanelTypeTable
st.ExpectUpdate(orgID, id, 1)
require.NoError(t, m.UpdateView(contextWithClaims(orgID, "updater@signoz.io"), orgID, id, updated))
stored := testSavedView(orgID, id, "updater@signoz.io", testPostableSavedView("renamed", savedviewtypes.SourceTraces))
stored.Name = existingName
stored.Data.Spec.PanelType = savedviewtypes.PanelTypeTable
st.ExpectGet(orgID, id, stored)
got, err := m.GetView(contextWithClaims(orgID, "creator@signoz.io"), orgID, id)
require.NoError(t, err)
assert.Equal(t, existingName, got.Name, "name must not change on update")
assert.Equal(t, "renamed", got.Data.Spec.DisplayName)
assert.Equal(t, savedviewtypes.SourceTraces, got.Source)
assert.Equal(t, savedviewtypes.PanelTypeTable, got.Data.Spec.PanelType)
assert.Equal(t, "updater@signoz.io", got.UpdatedBy)
require.NoError(t, st.AssertExpectations())
}
func TestModule_UpdateView_NotFound(t *testing.T) {
m, st := newTestStore()
orgID := valuer.GenerateUUID().StringValue()
ctx := contextWithClaims(orgID, "someone@signoz.io")
id := valuer.GenerateUUID()
st.ExpectUpdate(orgID, id, 0)
err := m.UpdateView(ctx, orgID, id, testUpdatableSavedView("does-not-exist", savedviewtypes.SourceLogs))
require.Error(t, err)
assert.True(t, errors.Ast(err, errors.TypeNotFound), "expected a not-found error, got %v", err)
require.NoError(t, st.AssertExpectations())
}
func TestModule_UpdateView_ScopedToOrg(t *testing.T) {
m, st := newTestStore()
orgB := valuer.GenerateUUID().StringValue()
id := valuer.GenerateUUID()
// Only an Update scoped to orgB's WHERE clause is registered; updating org
// A's view while authenticated as org B must not match it.
st.ExpectUpdate(orgB, id, 0)
err := m.UpdateView(contextWithClaims(orgB, "b@signoz.io"), orgB, id, testUpdatableSavedView("hijacked", savedviewtypes.SourceLogs))
require.Error(t, err, "org B must not be able to update org A's view")
assert.True(t, errors.Ast(err, errors.TypeNotFound))
require.NoError(t, st.AssertExpectations())
}
func TestModule_DeleteView(t *testing.T) {
m, st := newTestStore()
orgID := valuer.GenerateUUID().StringValue()
ctx := contextWithClaims(orgID, "creator@signoz.io")
id := valuer.GenerateUUID()
st.ExpectDelete(orgID, id, 1)
require.NoError(t, m.DeleteView(ctx, orgID, id))
require.NoError(t, st.AssertExpectations())
}
func TestModule_DeleteView_NotFound(t *testing.T) {
m, st := newTestStore()
orgID := valuer.GenerateUUID().StringValue()
ctx := contextWithClaims(orgID, "someone@signoz.io")
id := valuer.GenerateUUID()
st.ExpectDelete(orgID, id, 0)
err := m.DeleteView(ctx, orgID, id)
require.Error(t, err)
assert.True(t, errors.Ast(err, errors.TypeNotFound), "expected a not-found error, got %v", err)
require.NoError(t, st.AssertExpectations())
}
func TestModule_DeleteView_ScopedToOrg(t *testing.T) {
m, st := newTestStore()
orgB := valuer.GenerateUUID().StringValue()
id := valuer.GenerateUUID()
st.ExpectDelete(orgB, id, 0)
err := m.DeleteView(contextWithClaims(orgB, "b@signoz.io"), orgB, id)
require.Error(t, err, "org B must not be able to delete org A's view")
assert.True(t, errors.Ast(err, errors.TypeNotFound))
require.NoError(t, st.AssertExpectations())
}
func TestModule_GetViewsForFilters(t *testing.T) {
m, st := newTestStore()
orgID := valuer.GenerateUUID().StringValue()
ctx := contextWithClaims(orgID, "creator@signoz.io")
logsOverview := testSavedView(orgID, valuer.GenerateUUID(), "creator@signoz.io", testPostableSavedView("logs overview", savedviewtypes.SourceLogs))
logsErrors := testSavedView(orgID, valuer.GenerateUUID(), "creator@signoz.io", testPostableSavedView("logs errors", savedviewtypes.SourceLogs))
tracesOverview := testSavedView(orgID, valuer.GenerateUUID(), "creator@signoz.io", testPostableSavedView("traces overview", savedviewtypes.SourceTraces))
t.Run("filters by source page", func(t *testing.T) {
st.ExpectList(orgID, []*savedviewtypes.SavedView{logsOverview, logsErrors})
views, err := m.GetViewsForFilters(ctx, orgID, savedviewtypes.SourceLogs, "")
require.NoError(t, err)
assert.Len(t, views, 2)
})
t.Run("filters by name substring", func(t *testing.T) {
st.ExpectList(orgID, []*savedviewtypes.SavedView{logsErrors})
views, err := m.GetViewsForFilters(ctx, orgID, savedviewtypes.SourceLogs, "errors")
require.NoError(t, err)
require.Len(t, views, 1)
assert.Equal(t, "logs errors", views[0].Name)
})
t.Run("omitted source page returns everything, not nothing", func(t *testing.T) {
// Fixes a bug: source used to be an unconditional exact-match
// clause, so a zero-value source matched zero rows -- even though
// ListSavedViewsParams.Validate() treats a zero Source as valid
// ("no filter"). Store.List now only applies the source clause
// when it's non-zero.
st.ExpectList(orgID, []*savedviewtypes.SavedView{logsOverview, logsErrors, tracesOverview})
views, err := m.GetViewsForFilters(ctx, orgID, savedviewtypes.Source{}, "")
require.NoError(t, err)
assert.Len(t, views, 3)
})
t.Run("scoped to org", func(t *testing.T) {
otherOrgID := valuer.GenerateUUID().StringValue()
st.ExpectList(otherOrgID, nil)
views, err := m.GetViewsForFilters(ctx, otherOrgID, savedviewtypes.SourceLogs, "")
require.NoError(t, err)
assert.Empty(t, views)
})
require.NoError(t, st.AssertExpectations())
}
func TestModule_Collect(t *testing.T) {
m, st := newTestStore()
orgID := valuer.GenerateUUID()
logsA := testSavedView(orgID.StringValue(), valuer.GenerateUUID(), "creator@signoz.io", testPostableSavedView("logs a", savedviewtypes.SourceLogs))
logsB := testSavedView(orgID.StringValue(), valuer.GenerateUUID(), "creator@signoz.io", testPostableSavedView("logs b", savedviewtypes.SourceLogs))
tracesA := testSavedView(orgID.StringValue(), valuer.GenerateUUID(), "creator@signoz.io", testPostableSavedView("traces a", savedviewtypes.SourceTraces))
st.ExpectList(orgID.StringValue(), []*savedviewtypes.SavedView{logsA, logsB, tracesA})
stats, err := m.Collect(context.Background(), orgID)
require.NoError(t, err)
assert.Equal(t, int64(3), stats["savedview.count"])
assert.Equal(t, int64(2), stats["savedview.source.logs.count"])
assert.Equal(t, int64(1), stats["savedview.source.traces.count"])
require.NoError(t, st.AssertExpectations())
}

View File

@@ -1,109 +0,0 @@
package implsavedview
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type store struct {
sqlstore sqlstore.SQLStore
}
func NewStore(sqlstore sqlstore.SQLStore) savedviewtypes.Store {
return &store{sqlstore: sqlstore}
}
func (store *store) Create(ctx context.Context, view *savedviewtypes.SavedView) error {
_, err := store.sqlstore.BunDB().NewInsert().Model(view).Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "saved view with name %s already exists", view.Name)
}
return nil
}
func (store *store) Get(ctx context.Context, orgID string, id valuer.UUID) (*savedviewtypes.SavedView, error) {
var view savedviewtypes.SavedView
err := store.sqlstore.BunDB().NewSelect().Model(&view).Where("org_id = ? AND id = ?", orgID, id.StringValue()).Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, savedviewtypes.ErrCodeSavedViewNotFound, "saved view %s not found", id.StringValue())
}
normalizeSelectedFields(&view)
return &view, nil
}
func (store *store) Update(ctx context.Context, view *savedviewtypes.SavedView) error {
res, err := store.sqlstore.BunDB().NewUpdate().
Model(&savedviewtypes.SavedView{}).
Set("updated_at = ?, updated_by = ?, source = ?, data = ?",
view.UpdatedAt, view.UpdatedBy, view.Source, view.Data).
Where("id = ?", view.ID.StringValue()).
Where("org_id = ?", view.OrgID).
Exec(ctx)
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "error in updating saved view")
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "error in verifying the updated saved view")
}
if rowsAffected == 0 {
return errors.NewNotFoundf(savedviewtypes.ErrCodeSavedViewNotFound, "saved view %s not found", view.ID.StringValue())
}
return nil
}
func (store *store) Delete(ctx context.Context, orgID string, id valuer.UUID) error {
res, err := store.sqlstore.BunDB().NewDelete().
Model(&savedviewtypes.SavedView{}).
Where("id = ?", id.StringValue()).
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "error in deleting saved view")
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "error in verifying the deleted saved view")
}
if rowsAffected == 0 {
return errors.NewNotFoundf(savedviewtypes.ErrCodeSavedViewNotFound, "saved view %s not found", id.StringValue())
}
return nil
}
func (store *store) List(ctx context.Context, orgID string, source savedviewtypes.Source, name string) ([]*savedviewtypes.SavedView, error) {
var views []*savedviewtypes.SavedView
q := store.sqlstore.BunDB().NewSelect().Model(&views).
Where("org_id = ?", orgID).
Where("name LIKE ?", "%"+name+"%")
if !source.IsZero() {
q = q.Where("source = ?", source)
}
if err := q.Scan(ctx); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in getting saved views")
}
for _, view := range views {
normalizeSelectedFields(view)
}
return views, nil
}
// normalizeSelectedFields fixes up a scanned row's nil SelectedFields.
func normalizeSelectedFields(view *savedviewtypes.SavedView) {
if view.Data.Spec.SelectedFields == nil {
view.Data.Spec.SelectedFields = []telemetrytypes.TelemetryFieldKey{}
}
}

View File

@@ -4,19 +4,19 @@ import (
"context"
"net/http"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
GetViewsForFilters(ctx context.Context, orgID string, source savedviewtypes.Source, name string) ([]*savedviewtypes.SavedView, error)
GetViewsForFilters(ctx context.Context, orgID string, sourcePage string, name string, category string) ([]*v3.SavedView, error)
CreateView(ctx context.Context, orgID string, view savedviewtypes.PostableSavedView) (valuer.UUID, error)
CreateView(ctx context.Context, orgID string, view v3.SavedView) (valuer.UUID, error)
GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*savedviewtypes.SavedView, error)
GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*v3.SavedView, error)
UpdateView(ctx context.Context, orgID string, uuid valuer.UUID, view savedviewtypes.UpdatableSavedView) error
UpdateView(ctx context.Context, orgID string, uuid valuer.UUID, view v3.SavedView) error
DeleteView(ctx context.Context, orgID string, uuid valuer.UUID) error
@@ -33,22 +33,9 @@ type Handler interface {
// Updates the saved view
Update(http.ResponseWriter, *http.Request)
// Deletes the saved view. Shared by both API generations -- delete has no
// request/response body to reshape.
// Deletes the saved view
Delete(http.ResponseWriter, *http.Request)
// Lists the saved views
List(http.ResponseWriter, *http.Request)
// CreateV2 is the /api/v2/saved_views typed-spec variant of Create.
CreateV2(http.ResponseWriter, *http.Request)
// GetV2 is the /api/v2/saved_views typed-spec variant of Get.
GetV2(http.ResponseWriter, *http.Request)
// UpdateV2 is the /api/v2/saved_views typed-spec variant of Update.
UpdateV2(http.ResponseWriter, *http.Request)
// ListV2 is the /api/v2/saved_views typed-spec variant of List.
ListV2(http.ResponseWriter, *http.Request)
}

View File

@@ -88,6 +88,58 @@ func (handler *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusCreated, invites[0])
}
func (handler *handler) CreateBulkInvite(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
var req types.PostableBulkInviteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, err)
return
}
// Validate that the request contains users
if len(req.Invites) == 0 {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "no invites provided for invitation"))
return
}
_, err = handler.setter.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.IdentityID()), valuer.MustNewEmail(claims.Email), &req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, nil)
}
func (handler *handler) GetUserDeprecated(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
id := mux.Vars(r)["id"]
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
user, err := handler.getter.GetDeprecatedUserByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(id))
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, user)
}
func (handler *handler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -232,6 +284,33 @@ func (handler *handler) ListUsers(w http.ResponseWriter, r *http.Request) {
render.Success(w, http.StatusOK, users)
}
func (handler *handler) UpdateUserDeprecated(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
id := mux.Vars(r)["id"]
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
user := types.DeprecatedUser{User: &types.User{}}
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
render.Error(w, err)
return
}
updatedUser, err := handler.setter.UpdateUserDeprecated(ctx, valuer.MustNewUUID(claims.OrgID), id, &user)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, updatedUser)
}
func (handler *handler) UpdateUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -391,7 +470,7 @@ func (handler *handler) ResetPassword(w http.ResponseWriter, r *http.Request) {
defer cancel()
req := new(types.PostableResetPassword)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
render.Error(w, err)
return
}

View File

@@ -276,6 +276,92 @@ func (module *setter) CreatePendingInviteUser(ctx context.Context, identityID va
return user, nil
}
func (module *setter) UpdateUserDeprecated(ctx context.Context, orgID valuer.UUID, id string, user *types.DeprecatedUser) (*types.DeprecatedUser, error) {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return nil, err
}
existingUser, err := module.getter.GetDeprecatedUserByOrgIDAndID(ctx, orgID, valuer.MustNewUUID(id))
if err != nil {
return nil, err
}
if err := existingUser.ErrIfRoot(); err != nil {
return nil, errors.WithAdditionalf(err, "cannot update root user")
}
if err := existingUser.ErrIfDeleted(); err != nil {
return nil, errors.WithAdditionalf(err, "cannot update deleted user")
}
roleChange := user.Role != "" && user.Role != existingUser.Role
if roleChange {
selectors := []coretypes.Selector{
coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName),
}
err = module.authz.CheckWithTupleCreation(
ctx,
claims,
valuer.MustNewUUID(claims.OrgID),
authtypes.Relation{Verb: coretypes.VerbAssignee},
coretypes.NewResourceRole(),
selectors,
selectors,
)
if err != nil {
return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "only admins can change roles")
}
}
// make sure the user is not demoting self from admin
if roleChange && existingUser.ID == valuer.MustNewUUID(claims.IdentityID()) && existingUser.Role == types.RoleAdmin && user.Role != types.RoleAdmin {
return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot change self role")
}
if roleChange {
err = module.authz.ModifyGrant(ctx,
orgID,
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(existingUser.Role)},
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
authtypes.MustNewSubject(coretypes.NewResourceUser(), id, orgID, nil),
)
if err != nil {
return nil, err
}
}
existingUser.Update(user.DisplayName, user.Role)
// update the user - idempotent (this does analytics too so keeping it outside txn)
if err := module.UpdateAnyUserDeprecated(ctx, orgID, existingUser); err != nil {
return nil, err
}
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
if roleChange {
// delete old role entries and create new ones
if err := module.userRoleStore.DeleteUserRoles(ctx, existingUser.ID); err != nil {
return err
}
// create new ones
if err := module.createUserRoleEntries(ctx, existingUser.OrgID, existingUser.ID, []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)}); err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
return existingUser, nil
}
func (module *setter) UpdateUser(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, updatable *types.UpdatableUser) (*types.User, error) {
existingUser, err := module.getter.GetUserByOrgIDAndID(ctx, orgID, userID)
if err != nil {
@@ -315,6 +401,23 @@ func (module *setter) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user
return nil
}
func (module *setter) UpdateAnyUserDeprecated(ctx context.Context, orgID valuer.UUID, deprecateUser *types.DeprecatedUser) error {
user := types.NewUserFromDeprecatedUser(deprecateUser)
if err := module.store.UpdateUser(ctx, orgID, user); err != nil {
return err
}
traits := types.NewTraitsFromDeprecatedUser(deprecateUser)
module.analytics.IdentifyUser(ctx, user.OrgID.String(), user.ID.String(), traits)
module.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Updated", traits)
if err := module.tokenizer.DeleteIdentity(ctx, user.ID); err != nil {
return err
}
return nil
}
func (module *setter) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error {
user, err := module.store.GetUser(ctx, valuer.MustNewUUID(id))
if err != nil {

View File

@@ -34,9 +34,11 @@ type Setter interface {
// Initiate forgot password flow for a user
ForgotPassword(ctx context.Context, orgID valuer.UUID, email valuer.Email, frontendBaseURL string) error
UpdateUserDeprecated(ctx context.Context, orgID valuer.UUID, id string, user *types.DeprecatedUser) (*types.DeprecatedUser, error)
UpdateUser(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, updatable *types.UpdatableUser) (*types.User, error)
// UpdateAnyUser updates a user and persists the changes to the database along with the analytics and identity deletion.
UpdateAnyUserDeprecated(ctx context.Context, orgID valuer.UUID, deprecateUser *types.DeprecatedUser) error
UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user *types.User) error
DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error
@@ -107,13 +109,16 @@ type Getter interface {
type Handler interface {
// invite
CreateInvite(http.ResponseWriter, *http.Request)
CreateBulkInvite(http.ResponseWriter, *http.Request)
// users
ListUsersDeprecated(http.ResponseWriter, *http.Request)
ListUsers(http.ResponseWriter, *http.Request)
CreateUser(http.ResponseWriter, *http.Request)
UpdateUserDeprecated(http.ResponseWriter, *http.Request)
UpdateUser(http.ResponseWriter, *http.Request)
DeleteUser(http.ResponseWriter, *http.Request)
GetUserDeprecated(http.ResponseWriter, *http.Request)
GetUser(http.ResponseWriter, *http.Request)
GetMyUserDeprecated(http.ResponseWriter, *http.Request)
GetMyUser(http.ResponseWriter, *http.Request)

View File

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

View File

@@ -507,9 +507,9 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/explorer/views", am.ViewAccess(aH.Signoz.Handlers.SavedView.List)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/explorer/views", am.EditAccess(aH.Signoz.Handlers.SavedView.Create)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/explorer/views/{id}", am.ViewAccess(aH.Signoz.Handlers.SavedView.Get)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/explorer/views/{id}", am.EditAccess(aH.Signoz.Handlers.SavedView.Update)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/explorer/views/{id}", am.EditAccess(aH.Signoz.Handlers.SavedView.Delete)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/explorer/views/{viewId}", am.ViewAccess(aH.Signoz.Handlers.SavedView.Get)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/explorer/views/{viewId}", am.EditAccess(aH.Signoz.Handlers.SavedView.Update)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/explorer/views/{viewId}", am.EditAccess(aH.Signoz.Handlers.SavedView.Delete)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/event", am.ViewAccess(aH.registerEvent)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/services", am.ViewAccess(aH.getServices)).Methods(http.MethodPost) // Deprecated Usage, use the below endpoint /v2/services

View File

@@ -17,7 +17,6 @@ var (
CodeClickHouseSQLNotSingleStatement = errors.MustNewCode("clickhouse_sql_not_single_statement")
CodeClickHouseSQLNotSelect = errors.MustNewCode("clickhouse_sql_not_select")
CodeClickHouseSQLTableFunction = errors.MustNewCode("clickhouse_sql_table_function")
CodeClickHouseSQLReadingFunction = errors.MustNewCode("clickhouse_sql_reading_function")
CodeClickHouseSQLInternalDatabase = errors.MustNewCode("clickhouse_sql_internal_database")
CodeClickHouseSQLReadonlyOverride = errors.MustNewCode("clickhouse_sql_readonly_override")
)
@@ -44,25 +43,6 @@ var generatorTableFunctions = map[string]string{
var generatorTableFunctionsMessage = "allowed table functions are " + strings.Join(slices.Sorted(maps.Values(generatorTableFunctions)), ", ")
// readingFunctions reach a file, a model or the server binary while looking like ordinary
// scalar functions. They name no table and no database, so neither of the rules above sees
// them, and a wrapper that returns a number leaks what they read through the row count alone:
// numbers(length(file(x))) yields one row per byte.
//
// Keyed by the lowercased name, since ClickHouse resolves function names case-insensitively.
var readingFunctions = map[string]struct{}{
"file": {},
"catboostevaluate": {},
"demangle": {},
"addresstoline": {},
"addresstolinewithinlines": {},
"addresstosymbol": {},
}
// A dictionary can be backed by HTTP, ODBC or another database, and every one of the 42
// accessors carries this prefix.
const dictionaryFunctionPrefix = "dict"
// The parser's grammar has gaps against SQL that ClickHouse itself accepts.
func ErrIfStatementIsNotValid(query string) (err error) {
defer func() {
@@ -89,23 +69,11 @@ func ErrIfStatementIsNotValid(query string) (err error) {
visitor := &chparser.DefaultASTVisitor{Visit: func(node chparser.Expr) error {
switch expr := node.(type) {
case *chparser.TableExpr:
// Source table functions remain usable in ClickHouse read-only mode, and only a
// table position can be one. The parser also types a call inside a table function's
// argument list as a TableFunctionExpr, so asking every one of those refuses the
// numbers(intDiv(...)) that every dashboard writes. What can read from an argument
// is caught by name below instead.
source := expr.Expr
if alias, ok := source.(*chparser.AliasExpr); ok {
source = alias.Expr
}
tableFunction, ok := source.(*chparser.TableFunctionExpr)
if !ok {
return nil
}
name := functionName(tableFunction.Name)
case *chparser.TableFunctionExpr:
// Source table functions remain usable in ClickHouse read-only mode. Arguments are
// visited before this, so a read smuggled into one is already refused by the time
// an allowed generator gets here.
name := chparser.Format(expr.Name)
if _, ok := generatorTableFunctions[strings.ToLower(name)]; ok {
return nil
}
@@ -114,25 +82,6 @@ func ErrIfStatementIsNotValid(query string) (err error) {
NewInvalidInputf(CodeClickHouseSQLTableFunction, "ClickHouse table functions are not allowed in SQL queries: %s", name).
WithAdditional(generatorTableFunctionsMessage)
case *chparser.FunctionExpr:
return errIfFunctionReads(expr.Name.Name)
case *chparser.TableFunctionExpr:
// Reached for a call in an argument list, and for a table position ahead of the
// TableExpr above, since a node is visited after its children.
return errIfFunctionReads(functionName(expr.Name))
case *chparser.Path:
// ClickHouse reads `x IN db.table` as a select from that table, and a qualified name
// on the right of IN is a Path rather than a TableIdentifier.
if len(expr.Fields) < 2 {
return nil
}
if _, ok := internalDatabases[strings.ToLower(expr.Fields[0].Name)]; ok {
return errors.NewInvalidInputf(CodeClickHouseSQLInternalDatabase, "the ClickHouse %s database is not allowed in SQL queries", expr.Fields[0].Name)
}
case *chparser.TableIdentifier:
// Reading these is unaffected by ClickHouse read-only mode.
if expr.Database == nil {
@@ -162,22 +111,3 @@ func LogIfStatementIsNotValid(ctx context.Context, logger *slog.Logger, query st
logger.WarnContext(ctx, "clickhouse sql is not valid", errors.Attr(err), slog.String("query", query))
}
}
func errIfFunctionReads(name string) error {
lowered := strings.ToLower(name)
if _, ok := readingFunctions[lowered]; !ok && !strings.HasPrefix(lowered, dictionaryFunctionPrefix) {
return nil
}
return errors.NewInvalidInputf(CodeClickHouseSQLReadingFunction, "ClickHouse functions that read outside the telemetry tables are not allowed in SQL queries: %s", name)
}
// The parser spells a call's name as an Ident everywhere it can. Reading the field rather than
// formatting the node keeps the quoting out, so `numbers`(1) matches numbers.
func functionName(expr chparser.Expr) string {
if ident, ok := expr.(*chparser.Ident); ok {
return ident.Name
}
return chparser.Format(expr)
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestErrIfStatementIsNotValid_Pass(t *testing.T) {
@@ -15,12 +14,13 @@ func TestErrIfStatementIsNotValid_Pass(t *testing.T) {
name string
query string
}{
// Shapes a telemetry read is allowed to take.
{"Select", "SELECT region AS r, zone FROM metrics WHERE metric_name = 'cpu' GROUP BY region, zone"},
{"TrailingSemicolon", "SELECT count() FROM signoz_logs.distributed_logs_v2;"},
{"CommonTableExpression", "WITH t AS (SELECT fingerprint FROM signoz_metrics.time_series_v4) SELECT * FROM t"},
{"Join", "SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.b"},
{"GlobalIn", "SELECT a FROM t WHERE a GLOBAL IN (SELECT b FROM t2)"},
// https://github.com/AfterShip/clickhouse-sql-parser/pull/293
// GLOBAL parsed only when the join type was omitted, and only before IN. https://github.com/AfterShip/clickhouse-sql-parser/pull/293
{"GlobalLeftJoin", "SELECT * FROM t1 GLOBAL LEFT JOIN t2 ON t1.a = t2.a"},
{"GlobalNotIn", "SELECT a FROM t WHERE a GLOBAL NOT IN (SELECT b FROM t2)"},
{"Union", "SELECT * FROM t UNION ALL SELECT * FROM t2"},
@@ -29,34 +29,32 @@ func TestErrIfStatementIsNotValid_Pass(t *testing.T) {
{"UnrelatedSetting", "SELECT * FROM t SETTINGS max_threads = 4"},
{"TerminatedBlockComment", "SELECT /* keep me */ count() FROM t"},
{"BlockCommentMarkerInsideStringLiteral", "SELECT count() FROM t WHERE body = '/* not a comment'"},
// Looped forever before v0.5.2.
// The parser used to loop forever on this; it now reads the comment to the end of
// the input, so this doubles as a canary for that regression.
{"TrailingUnterminatedBlockComment", "SELECT count() FROM t /* unterminated"},
// Keyed on the database, not on the table name.
// The rule keys on the database, not on the table name.
{"TableNamedSystemInTelemetryDatabase", "SELECT * FROM signoz_logs.system"},
{"SignedLiteralAfterClosingParenSpaced", "SELECT (toUnixTimestamp(now()) - 3600)*1000000000"},
// order by interval
{"OrderByInterval", "SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval ORDER BY interval"},
{"OrderByIntervalAndDirection", "SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS `interval` ORDER BY `interval` ASC"},
// https://github.com/AfterShip/clickhouse-sql-parser/pull/296
// `interval` is a unit keyword, so unquoting it was rejected everywhere the parser
// expected a plain identifier. https://github.com/AfterShip/clickhouse-sql-parser/pull/296
{"OrderByUnquotedIntervalAsc", "SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval FROM t GROUP BY interval ORDER BY interval ASC"},
{"OrderByUnquotedIntervalDesc", "SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval FROM t GROUP BY interval ORDER BY interval DESC"},
{"UnquotedIntervalInGroupByTuple", "SELECT a FROM t GROUP BY (`service.name`, `service.version`, interval)"},
{"UnquotedIntervalProductionQuery", "SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval, resource_string_service$$name AS `service.name`, attributes_string['http.route'] AS `http.route`, quantile(0.95)(duration_nano) / 1000000000 AS value FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_string_service$$name = 'svc-a' AND resources_string['deployment.environment'] = 'dev' AND attributes_string['http.route'] = '/v1' AND http_method = 'POST' AND timestamp BETWEEN toDateTime(1784601720) AND toDateTime(1784602620) AND ts_bucket_start BETWEEN 1784601720 - 1800 AND 1784602620 GROUP BY `service.name`, `http.route`, interval ORDER BY interval ASC"},
// The fix backtracks, so this bounds the cost. https://github.com/AfterShip/clickhouse-sql-parser/pull/296#issuecomment-5150316367
// Separating the two readings of INTERVAL needs backtracking as per the current implementation which could have performance regressions.
// https://github.com/AfterShip/clickhouse-sql-parser/pull/296#issuecomment-5150316367
{"UnquotedIntervalRepeatedThirtyTimes", "SELECT interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval + interval AS total FROM t WHERE interval > 0 ORDER BY interval ASC"},
// `interval` was one of 37 such keywords. https://github.com/AfterShip/clickhouse-sql-parser/pull/305
{"UnquotedLimitInFunctionArgument", "SELECT sum(limit) FROM t"},
{"UnquotedLimitInArithmetic", "SELECT limit + 1 FROM t"},
{"UnquotedLimitInNegation", "SELECT abs(-limit) FROM t"},
{"UnquotedKeywordOperands", "SELECT sum(offset) + sum(format) + sum(settings) FROM t"},
{"UnquotedLimitProductionQuery", "WITH limit_value AS (SELECT cluster, region, value AS limit FROM t) SELECT region AS `Region`, sum(limit) AS `Capacity` FROM limit_value GROUP BY Region"},
{"SignedLiteralAfterClosingParenUnspaced", "SELECT now() AS ts, toFloat64(count()) AS value FROM ( SELECT attributes_string['TableName'] AS T, attributes_string['MissingId'] AS M, max(fromUnixTimestamp64Nano(timestamp)) AS last_seen, dateDiff('minute', min(fromUnixTimestamp64Nano(timestamp)), max(fromUnixTimestamp64Nano(timestamp))) AS age_min FROM signoz_logs.distributed_logs_v2 WHERE body='missing_map_record' AND timestamp >= (toUnixTimestamp(now())-3600)*1000000000 GROUP BY T, M ) WHERE age_min >= 20 AND last_seen >= now() - toIntervalMinute(8)"},
{"SignedLiteralAfterClosingParenMinimal", "SELECT (1)-1"},
{"TrimFunction", "SELECT trimBoth('/api/endpoint/', '/');"},
// https://github.com/AfterShip/clickhouse-sql-parser/pull/290
// The SQL-standard keyword-separated argument forms, which took commas only. https://github.com/AfterShip/clickhouse-sql-parser/pull/290
{"StandardTrimSyntax", "SELECT trim(BOTH ' ' FROM body) FROM t"},
{"StandardSubstringSyntax", "SELECT substring(body FROM 2 FOR 3) FROM t"},
{"StandardOverlaySyntax", "SELECT overlay(body PLACING 'x' FROM 2) FROM t"},
// The shape row generators get used for: a dense interval axis to CROSS JOIN a sparse series against.
// Row generators compute their rows from their arguments, so they read through nothing. This is the shape they get used for: a dense interval axis to CROSS JOIN a sparse series against.
{"NumbersTableFunction", "SELECT intervals.interval AS interval, active.cluster AS cluster, toFloat64(if(ts_data.has_data = 0, 0, 1)) AS value FROM ( SELECT DISTINCT JSONExtractString(labels, 'k8s.cluster.name') AS cluster FROM signoz_metrics.distributed_time_series_v4 WHERE metric_name = 'my_metric' AND unix_milli >= toUnixTimestamp(now() - INTERVAL 30 DAY) * 1000 HAVING cluster != '' ) AS active CROSS JOIN ( SELECT toStartOfInterval( toDateTime(toUnixTimestamp(now() - INTERVAL 30 MINUTE) + number * 60), INTERVAL 1 MINUTE ) AS interval FROM numbers(31) ) AS intervals LEFT JOIN ( SELECT toStartOfInterval( toDateTime(intDiv(s.unix_milli, 1000)), INTERVAL 1 MINUTE ) AS interval, JSONExtractString(ts.labels, 'k8s.cluster.name') AS cluster, 1 AS has_data FROM signoz_metrics.distributed_samples_v4 s INNER JOIN ( SELECT DISTINCT fingerprint, labels FROM signoz_metrics.distributed_time_series_v4 WHERE metric_name = 'my_metric' ) AS ts ON s.fingerprint = ts.fingerprint WHERE s.metric_name = 'my_metric' AND s.unix_milli >= toUnixTimestamp(now() - INTERVAL 30 MINUTE) * 1000 GROUP BY interval, cluster ) AS ts_data ON active.cluster = ts_data.cluster AND intervals.interval = ts_data.interval ORDER BY interval ASC"},
{"NumbersMtTableFunction", "SELECT * FROM numbers_mt(31)"},
{"ZerosTableFunction", "SELECT * FROM zeros(31)"},
@@ -65,16 +63,6 @@ func TestErrIfStatementIsNotValid_Pass(t *testing.T) {
{"GenerateSeriesSnakeCaseTableFunction", "SELECT * FROM generate_series(1, 10)"},
{"GeneratorTableFunctionUppercase", "SELECT * FROM NUMBERS(31)"},
{"GeneratorTableFunctionParenthesisedArgument", "SELECT * FROM NUMBERS((31))"},
// CAST in an argument was itself read as a table function. https://github.com/AfterShip/clickhouse-sql-parser/pull/307
{"CastInGeneratorTableFunctionArgument", "SELECT * FROM numbers(CAST(10 AS UInt64))"},
{"ScalarCallInGeneratorTableFunctionArgument", "SELECT * FROM numbers(intDiv(100, 2))"},
{"NestedScalarCallInGeneratorTableFunctionArgument", "SELECT * FROM numbers(greatest(1, intDiv(100, 2) + 1))"},
{"GeneratorTableFunctionProductionQuery", "WITH toInt64(1786029960000000000) AS start_ns, toInt64(1786031760000000000) AS end_ns, 300000000000 AS step_ns SELECT ts, toFloat64(sum(value)) AS value FROM (SELECT fromUnixTimestamp64Nano(start_ns + toInt64(number) * step_ns) AS ts, 0 AS value FROM numbers(greatest(1, intDiv(end_ns - start_ns, step_ns) + 1)) UNION ALL SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 5 minute) AS ts, count() AS value FROM signoz_logs.distributed_logs_v2 WHERE timestamp >= 1786029960000000000 AND timestamp <= 1786031760000000000 GROUP BY ts) GROUP BY ts ORDER BY ts"},
// The allow list keys on the bare name, so quoting must not hide a generator from it.
{"BacktickQuotedGeneratorTableFunction", "SELECT * FROM `numbers`(31)"},
{"DoubleQuotedGeneratorTableFunction", "SELECT * FROM \"numbers\"(31)"},
// Reads nothing: format builds a string, and shares its name with a table function.
{"ScalarFunctionNamedAfterATableFunction", "SELECT format('{} {}', a, b) FROM t"},
{"GeneratorTableFunctionInJoin", "SELECT * FROM signoz_logs.distributed_logs_v2 AS l CROSS JOIN numbers(31) AS n"},
{"GeneratorTableFunctionInCommonTableExpression", "WITH axis AS (SELECT number FROM numbers(31)) SELECT * FROM axis"},
{"GeneratorTableFunctionInWhereSubquery", "SELECT * FROM t WHERE a IN (SELECT number FROM numbers(31))"},
@@ -83,7 +71,8 @@ func TestErrIfStatementIsNotValid_Pass(t *testing.T) {
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
// Bounded because a parser that backtracks without memoising hangs rather than returning.
// Bounded rather than called directly: a parser that backtracks without memoising
// hangs instead of returning. Every case here parses in well under a millisecond.
errC := make(chan error, 1)
go func() { errC <- ErrIfStatementIsNotValid(testCase.query) }()
@@ -103,57 +92,46 @@ func TestErrIfStatementIsNotValid_Fail(t *testing.T) {
query string
expectedCode errors.Code
}{
// Not a single statement, or not a statement at all.
{"Empty", "", CodeClickHouseSQLNotSingleStatement},
{"UnterminatedBlockCommentOnly", "/* x", CodeClickHouseSQLUnparseable},
{"Unparseable", "SELECT FROM WHERE", CodeClickHouseSQLUnparseable},
{"MultipleStatements", "SELECT 1; DROP TABLE signoz_logs.logs_v2", CodeClickHouseSQLNotSingleStatement},
// Parses, but is not a SELECT.
{"Drop", "DROP TABLE signoz_logs.logs_v2", CodeClickHouseSQLNotSelect},
{"Insert", "INSERT INTO signoz_logs.logs_v2 SELECT * FROM signoz_logs.logs_v2", CodeClickHouseSQLNotSelect},
{"AlterDelete", "ALTER TABLE signoz_logs.logs_v2 DELETE WHERE 1 = 1", CodeClickHouseSQLNotSelect},
{"CreateTable", "CREATE TABLE evil (a Int) ENGINE = Memory", CodeClickHouseSQLNotSelect},
{"Grant", "GRANT ALL ON *.* TO admin", CodeClickHouseSQLNotSelect},
{"Set", "SET readonly = 0", CodeClickHouseSQLNotSelect},
// Both panicked before v0.5.5. https://github.com/AfterShip/clickhouse-sql-parser/pull/306
{"UnparseableDefaultExpression", "CREATE TABLE t (a String DEFAULT foo(b FROM 2)) ENGINE = Memory", CodeClickHouseSQLUnparseable},
{"TrailingOperatorInDefaultExpression", "CREATE TABLE t (a String DEFAULT 1 +) ENGINE = Memory", CodeClickHouseSQLUnparseable},
// Rejected outright rather than classified.
// The parser still dereferences nil on a DEFAULT expression it cannot read, so the recover is what turns this into a rejection rather than a crash.
{"UnparseableDefaultExpression", "CREATE TABLE t (a String DEFAULT foo(b FROM 2)) ENGINE = Memory", CodeClickHouseSQLParserPanic},
// These the parser rejects outright rather than classifying.
{"ShowGrants", "SHOW GRANTS", CodeClickHouseSQLUnparseable},
{"IntoOutfile", "SELECT * FROM t INTO OUTFILE '/tmp/x.csv'", CodeClickHouseSQLUnparseable},
// Table functions, which read through something other than a telemetry table.
{"UrlTableFunction", "SELECT * FROM url('http://attacker.example/x', CSV, 'a String')", CodeClickHouseSQLTableFunction},
// file is also a scalar function, so the reading rule reaches it before the table rule does.
{"FileTableFunction", "SELECT * FROM file('/etc/passwd', CSV, 'a String')", CodeClickHouseSQLReadingFunction},
{"FileTableFunction", "SELECT * FROM file('/etc/passwd', CSV, 'a String')", CodeClickHouseSQLTableFunction},
{"ExecutableTableFunction", "SELECT * FROM executable('script.sh', CSV, 'a String')", CodeClickHouseSQLTableFunction},
{"TableFunctionInJoin", "SELECT * FROM t1 JOIN url('http://x', CSV, 'a String') u ON 1 = 1", CodeClickHouseSQLTableFunction},
{"TableFunctionInCommonTableExpression", "WITH c AS (SELECT * FROM url('http://x', CSV, 'a String')) SELECT * FROM c", CodeClickHouseSQLTableFunction},
{"TableFunctionInWhereSubquery", "SELECT * FROM t WHERE a IN (SELECT * FROM url('http://x', CSV, 'a String'))", CodeClickHouseSQLTableFunction},
{"TableFunctionInWhereSubquery", "SELECT * FROM t WHERE a IN (SELECT * FROM file('/etc/passwd', CSV, 'a String'))", CodeClickHouseSQLTableFunction},
{"TableFunctionInUnion", "SELECT * FROM t UNION ALL SELECT * FROM url('http://x', CSV, 'a String')", CodeClickHouseSQLTableFunction},
// Reach an internal database without naming one, so only the table-function rule sees them.
// These reach the internal databases without ever naming one, so the table-function rule is the only thing that sees them.
{"MergeTableFunction", "SELECT * FROM merge('system', '.*')", CodeClickHouseSQLTableFunction},
{"RemoteTableFunction", "SELECT * FROM remote('other-host', 'system.users')", CodeClickHouseSQLTableFunction},
{"ClusterTableFunction", "SELECT * FROM cluster('c', 'system.users')", CodeClickHouseSQLTableFunction},
// Pure, but excluded: generateRandom is unbounded, and values adds nothing over an array literal.
// Pure, but excluded: generateRandom streams rows the arguments do not bound, and values has no use here that an array literal does not already cover.
{"GenerateRandomTableFunction", "SELECT * FROM generateRandom('a UInt64')", CodeClickHouseSQLTableFunction},
{"ValuesTableFunction", "SELECT * FROM values('a UInt64', 1, 2)", CodeClickHouseSQLTableFunction},
// Arguments are visited first, so an allowed generator is not a wrapper to smuggle a read through.
// Arguments are visited before the table function itself, so allowing a generator does not give anyone a wrapper to smuggle a read through.
{"InternalDatabaseInsideAllowedTableFunction", "SELECT * FROM numbers((SELECT count() FROM system.users))", CodeClickHouseSQLInternalDatabase},
{"InternalDatabaseJoinedOntoAllowedTableFunction", "SELECT * FROM numbers(31) AS n JOIN system.users AS u ON 1 = 1", CodeClickHouseSQLInternalDatabase},
{"InternalDatabaseUnionedWithAllowedTableFunction", "SELECT number FROM numbers(31) UNION ALL SELECT name FROM system.users", CodeClickHouseSQLInternalDatabase},
{"RefusedTableFunctionJoinedOntoAllowedTableFunction", "SELECT * FROM numbers(31) AS n JOIN url('http://x', CSV, 'a String') AS u ON 1 = 1", CodeClickHouseSQLTableFunction},
{"RefusedTableFunctionInsideAllowedTableFunction", "SELECT * FROM numbers((SELECT count() FROM url('http://x', CSV, 'a String')))", CodeClickHouseSQLTableFunction},
{"RefusedTableFunctionInsideAllowedTableFunction", "SELECT * FROM numbers((SELECT count() FROM file('/etc/passwd', CSV, 'a String')))", CodeClickHouseSQLTableFunction},
{"InternalDatabaseInsideAllowedTableFunctionCommonTableExpression", "WITH axis AS (SELECT * FROM numbers((SELECT count() FROM system.users))) SELECT * FROM axis", CodeClickHouseSQLInternalDatabase},
// Read a file, a dictionary or the server binary without naming a table, so neither the table rule nor the database rule sees them. The row count alone is an oracle: numbers(length(file(x))) returns one row per byte.
{"ScalarFileFunction", "SELECT file('/etc/passwd')", CodeClickHouseSQLReadingFunction},
{"ScalarFileFunctionInWhere", "SELECT * FROM t WHERE length(file('/etc/passwd')) > 0", CodeClickHouseSQLReadingFunction},
{"ScalarFileFunctionInGeneratorTableFunctionArgument", "SELECT * FROM numbers(length(file('/etc/passwd')))", CodeClickHouseSQLReadingFunction},
{"DictionaryFunction", "SELECT dictGetUInt64('d', 'k', toUInt64(1))", CodeClickHouseSQLReadingFunction},
{"DictionaryFunctionUppercase", "SELECT DICTGETSTRING('d', 'k', toUInt64(1))", CodeClickHouseSQLReadingFunction},
{"DictionaryFunctionInGeneratorTableFunctionArgument", "SELECT * FROM numbers(dictGetUInt64('d', 'k', toUInt64(1)))", CodeClickHouseSQLReadingFunction},
{"IntrospectionFunction", "SELECT demangle(addressToSymbol(toUInt64(1)))", CodeClickHouseSQLReadingFunction},
{"ModelEvaluationFunction", "SELECT catboostEvaluate('/model.bin', 1)", CodeClickHouseSQLReadingFunction},
// ClickHouse reads `x IN table` as `x IN (SELECT * FROM table)`, and a qualified name there is a Path rather than a TableIdentifier.
{"InternalDatabaseInInOperator", "SELECT * FROM t WHERE a IN system.users", CodeClickHouseSQLInternalDatabase},
{"InternalDatabaseInGlobalInOperator", "SELECT * FROM t WHERE a GLOBAL IN system.users", CodeClickHouseSQLInternalDatabase},
{"InternalDatabaseInNotInOperator", "SELECT * FROM t WHERE a NOT IN system.users", CodeClickHouseSQLInternalDatabase},
// Internal databases, which hold grants and server metadata rather than telemetry.
{"SystemUsers", "SELECT * FROM system.users", CodeClickHouseSQLInternalDatabase},
{"SystemUppercase", "SELECT * FROM SYSTEM.USERS", CodeClickHouseSQLInternalDatabase},
{"SystemQuoted", "SELECT count() FROM `system`.`tables`", CodeClickHouseSQLInternalDatabase},
@@ -161,7 +139,7 @@ func TestErrIfStatementIsNotValid_Fail(t *testing.T) {
{"SystemInJoin", "SELECT * FROM signoz_logs.distributed_logs_v2 AS l JOIN system.users AS u ON 1 = 1", CodeClickHouseSQLInternalDatabase},
{"SystemInIntersect", "SELECT * FROM t INTERSECT SELECT * FROM system.users", CodeClickHouseSQLInternalDatabase},
{"InformationSchema", "SELECT * FROM information_schema.tables", CodeClickHouseSQLInternalDatabase},
// Takes precedence over the setting the caller applies.
// A query-level setting takes precedence over the one the caller applies.
{"ReadonlySettingOverride", "SELECT * FROM t SETTINGS readonly = 0", CodeClickHouseSQLReadonlyOverride},
{"ReadonlySettingOverrideAmongOthers", "SELECT * FROM t SETTINGS max_threads = 4, readonly = 0", CodeClickHouseSQLReadonlyOverride},
}
@@ -170,33 +148,7 @@ func TestErrIfStatementIsNotValid_Fail(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
err := ErrIfStatementIsNotValid(testCase.query)
// Required rather than asserted: errors.Asc dereferences the error it is given.
require.Error(t, err)
assert.True(t, errors.Asc(err, testCase.expectedCode), "expected code %s, got %v", testCase.expectedCode, err)
})
}
}
func TestErrIfStatementIsNotValid_ShouldPassButFails(t *testing.T) {
testCases := []struct {
name string
query string
expectedCode errors.Code
}{
// The left operand commits the parser to a subquery, leaving the operator nowhere to bind. Parenthesising only the right operand is fine.
{"ParenthesisedUnionLeftOperand", "SELECT a FROM ((SELECT 1 AS a) UNION ALL (SELECT 2 AS a))", CodeClickHouseSQLUnparseable},
{"ParenthesisedExceptLeftOperand", "SELECT a FROM ((SELECT 1 AS a) EXCEPT (SELECT 2 AS a))", CodeClickHouseSQLUnparseable},
{"ParenthesisedUnionLeftOperandAtStatementLevel", "(SELECT 1 AS a) UNION ALL (SELECT 2 AS a)", CodeClickHouseSQLUnparseable},
// The one keyword PR 305 left behind, because ON also opens a join condition.
{"UnquotedOnAsColumnName", "SELECT on + 1 FROM t", CodeClickHouseSQLUnparseable},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
err := ErrIfStatementIsNotValid(testCase.query)
// Required rather than asserted: errors.Asc dereferences the error it is given.
require.Error(t, err)
assert.Error(t, err)
assert.True(t, errors.Asc(err, testCase.expectedCode), "expected code %s, got %v", testCase.expectedCode, err)
})
}

View File

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

View File

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

View File

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

View File

@@ -139,7 +139,7 @@ func NewModules(
OrgGetter: orgGetter,
OrgSetter: orgSetter,
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()),
SavedView: implsavedview.NewModule(implsavedview.NewStore(sqlstore)),
SavedView: implsavedview.NewModule(sqlstore),
Apdex: implapdex.NewModule(sqlstore),
Dashboard: dashboard,
UserSetter: userSetter,

View File

@@ -30,7 +30,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/savedview"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
@@ -89,7 +88,6 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ tracedetail.Handler }{},
struct{ ruler.Handler }{},
struct{ statsreporter.Handler }{},
struct{ savedview.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -235,8 +235,6 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewFillDashboardSpecCollectionsFactory(sqlstore, dashboardStore),
sqlmigration.NewScrubEmailChannelTransportFactory(sqlstore),
sqlmigration.NewAddDashboardTuplesFactory(sqlstore),
sqlmigration.NewRestructureSavedViewSpecFactory(sqlstore, sqlschema),
sqlmigration.NewAddSavedViewTuplesFactory(sqlstore),
)
}
@@ -337,7 +335,6 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.TraceDetail,
handlers.RulerHandler,
handlers.StatsHandler,
handlers.SavedView,
),
)
}

View File

@@ -1,304 +0,0 @@
package sqlmigration
import (
"context"
"crypto/rand"
"database/sql"
"encoding/json"
"log/slog"
"strings"
"time"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
)
type restructureSavedViewSpec struct {
store sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
settings factory.ProviderSettings
}
func NewRestructureSavedViewSpecFactory(store sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("restructure_saved_view_spec"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &restructureSavedViewSpec{store: store, sqlschema: sqlschema, settings: ps}, nil
})
}
func (migration *restructureSavedViewSpec) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
// legacySavedViewCompositeQuery is the bare shape saved_view.data held
// before this migration -- just the relevant fields of composite query.
// Queries is kept as raw JSON since the migration only needs to relocate it, not interpret it.
type legacySavedViewCompositeQuery struct {
PanelType string `json:"panelType"`
Queries json.RawMessage `json:"queries"`
}
// legacySavedViewExtraData mirrors the frontend defined extraData JSON shape.
type legacySavedViewExtraData struct {
Color string `json:"color,omitempty"`
SelectColumns json.RawMessage `json:"selectColumns,omitempty"`
Format string `json:"format,omitempty"`
MaxLines int `json:"maxLines,omitempty"`
FontSize string `json:"fontSize,omitempty"`
}
type savedViewDisplay struct {
MaxLines int `json:"maxLines"`
FontSize string `json:"fontSize"`
Format string `json:"format"`
Color string `json:"color"`
}
type savedViewSpec struct {
DisplayName string `json:"displayName"`
PanelType string `json:"panelType"`
Queries json.RawMessage `json:"queries"`
SelectedFields json.RawMessage `json:"selectedFields"`
Display savedViewDisplay `json:"display"`
}
type savedViewData struct {
SchemaVersion string `json:"schemaVersion"`
Spec savedViewSpec `json:"spec"`
}
const migrationSavedViewNameSuffixLen = 8
// slugifySavedViewName turns a pre-existing free-text saved view name and is copy of
// dashboardtypes.generateDashboardName.
func slugifySavedViewName(displayName string) string {
const dns1123LabelMaxLen = 63
suffixAlphabet := []byte("abcdefghijklmnopqrstuvwxyz0123456789")
var b strings.Builder
b.Grow(len(displayName))
prevHyphen := false
for _, r := range strings.ToLower(displayName) {
switch {
case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'):
b.WriteRune(r)
prevHyphen = false
case b.Len() > 0 && !prevHyphen:
b.WriteByte('-')
prevHyphen = true
}
}
prefix := strings.TrimRight(b.String(), "-")
suffix := make([]byte, migrationSavedViewNameSuffixLen)
if _, err := rand.Read(suffix); err != nil {
panic(err)
}
for i := range suffix {
suffix[i] = suffixAlphabet[int(suffix[i])%len(suffixAlphabet)]
}
maxPrefix := dns1123LabelMaxLen - 1 - migrationSavedViewNameSuffixLen
if len(prefix) > maxPrefix {
prefix = strings.TrimRight(prefix[:maxPrefix], "-")
}
if prefix == "" {
return string(suffix)
}
return prefix + "-" + string(suffix)
}
// storableLegacySavedView is the shape of the `saved_views` table before this migration.
type storableLegacySavedView struct {
bun.BaseModel `bun:"table:saved_views"`
ID string `bun:"id"`
Name string `bun:"name"`
SourcePage string `bun:"source_page"`
Data string `bun:"data"`
ExtraData string `bun:"extra_data"`
OrgID string `bun:"org_id"`
CreatedAt time.Time `bun:"created_at"`
UpdatedAt time.Time `bun:"updated_at"`
CreatedBy string `bun:"created_by"`
UpdatedBy string `bun:"updated_by"`
}
// storableSavedView is the shape of the `saved_view` table this migration creates.
type storableSavedView struct {
bun.BaseModel `bun:"table:saved_view"`
ID string `bun:"id,pk,type:text"`
OrgID string `bun:"org_id,type:text,notnull"`
Name string `bun:"name,type:text,notnull"`
Source string `bun:"source,type:text,notnull"`
Data string `bun:"data,type:text,notnull"`
CreatedAt time.Time `bun:"created_at,notnull"`
UpdatedAt time.Time `bun:"updated_at,notnull"`
CreatedBy string `bun:"created_by,type:text,notnull"`
UpdatedBy string `bun:"updated_by,type:text,notnull"`
}
func (migration *restructureSavedViewSpec) Up(ctx context.Context, db *bun.DB) error {
// check if the `saved_view` table already exists
if _, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("saved_view")); err == nil {
return nil
}
savedViewsTable, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("saved_views"))
if err != nil {
return err
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var oldSavedViews []*storableLegacySavedView
if err := tx.NewSelect().Model(&oldSavedViews).Scan(ctx); err != nil && err != sql.ErrNoRows {
return err
}
var orgIDs []string
if err := tx.NewSelect().Model((*types.Organization)(nil)).Column("id").Scan(ctx, &orgIDs); err != nil {
return err
}
validOrgIDs := make(map[string]struct{}, len(orgIDs))
for _, id := range orgIDs {
validOrgIDs[id] = struct{}{}
}
// drop table `saved_views`
for _, sql := range migration.sqlschema.Operator().DropTable(savedViewsTable) {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
// create table `saved_view` with the final required schema
for _, sql := range migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "saved_view",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "name", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "source", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "data", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "created_by", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "updated_by", DataType: sqlschema.DataTypeText, Nullable: false},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{"id"},
},
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
{
ReferencingColumnName: sqlschema.ColumnName("org_id"),
ReferencedTableName: sqlschema.TableName("organizations"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
},
}) {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
// convert old saved views to the new shape
newSavedViews := make([]*storableSavedView, 0, len(oldSavedViews))
var skipped, failed int
for _, old := range oldSavedViews {
if old.OrgID == "" {
skipped++
continue // orphaned row from a pre-existing org_id backfill gap; nothing sane to attach it to
}
// to avoid foreign key constraint issues
if _, ok := validOrgIDs[old.OrgID]; !ok {
skipped++
migration.settings.Logger.WarnContext(ctx, "saved view references an org that no longer exists, skipping", slog.String("org_id", old.OrgID), slog.String("saved_view_id", old.ID))
continue
}
var compositeQuery legacySavedViewCompositeQuery
if err := json.Unmarshal([]byte(old.Data), &compositeQuery); err != nil {
failed++
migration.settings.Logger.WarnContext(ctx, "failed to unmarshal saved view data, skipping", slog.String("org_id", old.OrgID), slog.String("saved_view_id", old.ID), slog.Any("error", err))
continue // skip the row on error rather than fail the whole migration
}
var extraData legacySavedViewExtraData
if old.ExtraData != "" {
// best-effort: malformed/older extraData shapes never fail the migration,
// they just leave selectedFields/display empty.
_ = json.Unmarshal([]byte(old.ExtraData), &extraData)
}
dataJSON, err := json.Marshal(savedViewData{
SchemaVersion: "v2",
Spec: savedViewSpec{
DisplayName: old.Name,
PanelType: compositeQuery.PanelType,
Queries: compositeQuery.Queries,
SelectedFields: extraData.SelectColumns,
Display: savedViewDisplay{
MaxLines: extraData.MaxLines,
FontSize: extraData.FontSize,
Format: extraData.Format,
Color: extraData.Color,
},
},
})
if err != nil {
return err
}
// Existing names were free text (no slug constraints); the free-text
// value is preserved verbatim as data.spec.displayName above, and name is
// replaced with a fresh slug so it satisfies the new DNS-1123 + (org_id,
// name) uniqueness rules.
newSavedViews = append(newSavedViews, &storableSavedView{
ID: old.ID,
OrgID: old.OrgID,
Name: slugifySavedViewName(old.Name),
Source: old.SourcePage,
Data: string(dataJSON),
CreatedAt: old.CreatedAt,
UpdatedAt: old.UpdatedAt,
CreatedBy: old.CreatedBy,
UpdatedBy: old.UpdatedBy,
})
}
if len(newSavedViews) > 0 {
if _, err := tx.NewInsert().Model(&newSavedViews).Exec(ctx); err != nil {
return err
}
}
migration.settings.Logger.InfoContext(ctx, "restructured saved views", slog.Int("total", len(oldSavedViews)), slog.Int("migrated", len(newSavedViews)), slog.Int("skipped", skipped), slog.Int("failed", failed))
// add unique index on (org_id, name)
for _, sql := range migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{
TableName: "saved_view",
ColumnNames: []sqlschema.ColumnName{"org_id", "name"},
}) {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
return tx.Commit()
}
func (migration *restructureSavedViewSpec) Down(context.Context, *bun.DB) error {
// this migration is not reversible as we're transforming the structure
return nil
}

View File

@@ -1,144 +0,0 @@
package sqlmigration
import (
"context"
"database/sql"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/migrate"
)
type addSavedViewTuples struct {
sqlstore sqlstore.SQLStore
}
func NewAddSavedViewTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_saved_view_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addSavedViewTuples{sqlstore: sqlstore}, nil
})
}
func (migration *addSavedViewTuples) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addSavedViewTuples) 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
// saved-view moved from the legacy ViewAccess/EditAccess role gate to
// CheckResources, which on enterprise requires real tuples -- existing orgs
// never had these written, only new orgs get them from the registry at bootstrap.
tuples := []migrationTuple{
{authtypes.SigNozAdminRoleName, "metaresource", "saved-view", "create"},
{authtypes.SigNozAdminRoleName, "metaresource", "saved-view", "read"},
{authtypes.SigNozAdminRoleName, "metaresource", "saved-view", "update"},
{authtypes.SigNozAdminRoleName, "metaresource", "saved-view", "delete"},
{authtypes.SigNozAdminRoleName, "metaresource", "saved-view", "list"},
{authtypes.SigNozEditorRoleName, "metaresource", "saved-view", "create"},
{authtypes.SigNozEditorRoleName, "metaresource", "saved-view", "read"},
{authtypes.SigNozEditorRoleName, "metaresource", "saved-view", "update"},
{authtypes.SigNozEditorRoleName, "metaresource", "saved-view", "delete"},
{authtypes.SigNozEditorRoleName, "metaresource", "saved-view", "list"},
{authtypes.SigNozViewerRoleName, "metaresource", "saved-view", "read"},
{authtypes.SigNozViewerRoleName, "metaresource", "saved-view", "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
}
}
}
}
return tx.Commit()
}
func (migration *addSavedViewTuples) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -0,0 +1,155 @@
package aistatementbuilder
import (
"context"
"testing"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/require"
)
// Span list with a mixed filter: gen_ai spans matching the span-level part, in
// traces whose window-clipped aggregates satisfy the trace-level part (the
// __trace_scope qualification on the delegated path).
func TestBuild_FullSQL_SpanList_TraceScoped(t *testing.T) {
b := newTestBuilder(t)
stmt, err := b.Build(context.Background(), valuer.UUID{}, testStartMs, testEndMs, qbtypes.RequestTypeRaw,
qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Signal: telemetrytypes.SignalTraces,
Filter: &qbtypes.Filter{Expression: "gen_ai.request.model = 'gpt-4o-mini' AND trace.output_tokens > 1000"},
Limit: 10,
}, nil)
require.NoError(t, err)
assertSQLEqual(t, `
WITH __trace_scope AS (
SELECT trace_id,
sum(multiIf(mapContains(attributes_number, 'gen_ai.usage.output_tokens'), toFloat64(attributes_number['gen_ai.usage.output_tokens']), NULL)) AS output_tokens
FROM signoz_traces.distributed_signoz_index_v3
WHERE timestamp >= '1747947419000000000'
AND timestamp < '1747983448000000000'
AND ts_bucket_start >= 1747945619
AND ts_bucket_start <= 1747983448
AND ((mapContains(attributes_string, 'gen_ai.request.model') OR mapContains(attributes_string, 'gen_ai.tool.name') OR mapContains(attributes_string, 'gen_ai.agent.name')))
GROUP BY trace_id
HAVING output_tokens > 1000
)
SELECT timestamp AS __SELECT_KEY_0_timestamp, trace_id AS __SELECT_KEY_1_trace_id, span_id AS __SELECT_KEY_2_span_id,
trace_state AS __SELECT_KEY_3_trace_state, parent_span_id AS __SELECT_KEY_4_parent_span_id, flags AS __SELECT_KEY_5_flags,
name AS __SELECT_KEY_6_name, kind AS __SELECT_KEY_7_kind, kind_string AS __SELECT_KEY_8_kind_string, duration_nano AS __SELECT_KEY_9_duration_nano,
status_code AS __SELECT_KEY_10_status_code, status_message AS __SELECT_KEY_11_status_message,
status_code_string AS __SELECT_KEY_12_status_code_string, events AS __SELECT_KEY_13_events, links AS __SELECT_KEY_14_links,
response_status_code AS __SELECT_KEY_15_response_status_code, external_http_url AS __SELECT_KEY_16_external_http_url,
http_url AS __SELECT_KEY_17_http_url, external_http_method AS __SELECT_KEY_18_external_http_method,
http_method AS __SELECT_KEY_19_http_method, http_host AS __SELECT_KEY_20_http_host, db_name AS __SELECT_KEY_21_db_name,
db_operation AS __SELECT_KEY_22_db_operation, has_error AS __SELECT_KEY_23_has_error, is_remote AS __SELECT_KEY_24_is_remote,
attributes_string, attributes_number, attributes_bool, resources_string
FROM signoz_traces.distributed_signoz_index_v3
WHERE trace_id GLOBAL IN (SELECT trace_id FROM __trace_scope)
AND (((mapContains(attributes_string, 'gen_ai.request.model')
OR mapContains(attributes_string, 'gen_ai.tool.name')
OR mapContains(attributes_string, 'gen_ai.agent.name')))
AND ((attributes_string['gen_ai.request.model'] = 'gpt-4o-mini'
AND mapContains(attributes_string, 'gen_ai.request.model'))))
AND timestamp >= '1747947419000000000'
AND timestamp < '1747983448000000000'
AND ts_bucket_start >= 1747945619
AND ts_bucket_start <= 1747983448
LIMIT 10
`, stmt)
}
// A raw filter mixing a resource attribute with a trace-level condition: the resource
// part flows through the delegate's fingerprint machinery (__resource_filter CTE),
// the trace-level part becomes the __trace_scope qualification.
func TestBuild_SpanList_ResourcePlusTraceFilter(t *testing.T) {
b := newTestBuilder(t)
stmt, err := b.Build(context.Background(), valuer.UUID{}, testStartMs, testEndMs, qbtypes.RequestTypeRaw,
qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Signal: telemetrytypes.SignalTraces,
Filter: &qbtypes.Filter{Expression: "resource.service.name = 'checkout' AND trace.output_tokens > 1000"},
Limit: 10,
}, nil)
require.NoError(t, err)
got := renderSQL(t, stmt)
require.Contains(t, got, "__resource_filter AS (")
require.Contains(t, got, "resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
require.Contains(t, got, "__trace_scope AS (")
require.Contains(t, got, "trace_id GLOBAL IN (SELECT trace_id FROM __trace_scope)")
require.Contains(t, got, "HAVING output_tokens > 1000")
}
// Without a trace-level condition nothing changes: the span list stays a single
// gated span scan (no __trace_scope CTE).
func TestBuild_SpanList_NoTraceFilter_NoScope(t *testing.T) {
b := newTestBuilder(t)
stmt, err := b.Build(context.Background(), valuer.UUID{}, testStartMs, testEndMs, qbtypes.RequestTypeRaw,
qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Signal: telemetrytypes.SignalTraces,
Filter: &qbtypes.Filter{Expression: "gen_ai.request.model = 'gpt-4o-mini'"},
Limit: 10,
}, nil)
require.NoError(t, err)
require.NotContains(t, stmt.Query, "__trace_scope")
}
// The span-list trace-level filter shares the trace list's rules: output-only
// aggregates are rejected, OR-mixing the two classes is rejected, and explicitly
// trace-level order keys get a targeted error — while bare span columns that happen
// to share a name with an aggregate alias (duration_nano) stay orderable.
func TestBuild_SpanList_TraceFilter_Validation(t *testing.T) {
b := newTestBuilder(t)
build := func(q qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]) error {
q.Signal = telemetrytypes.SignalTraces
_, err := b.Build(context.Background(), valuer.UUID{}, testStartMs, testEndMs, qbtypes.RequestTypeRaw, q, nil)
return err
}
err := build(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Filter: &qbtypes.Filter{Expression: "trace.span_count > 3"},
})
require.ErrorContains(t, err, `aggregate "span_count" cannot be used`)
err = build(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Filter: &qbtypes.Filter{Expression: "trace.output_tokens > 1000 OR kind_string = 'Client'"},
})
require.ErrorContains(t, err, "cannot be combined")
err = build(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Order: []qbtypes.OrderBy{{Key: qbtypes.OrderByKey{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "trace.output_tokens"}}}},
})
require.ErrorContains(t, err, `ordering the span list by trace-level aggregate "trace.output_tokens" is not supported`)
err = build(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Order: []qbtypes.OrderBy{{Key: qbtypes.OrderByKey{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "duration_nano"}}, Direction: qbtypes.OrderDirectionDesc}},
Limit: 10,
})
require.NoError(t, err, "bare duration_nano is a span column, not a trace-level key")
}
// Variables in a trace-level condition on the span list get the trace list's
// treatment: substituted as literals, __all__ drops the condition (no scope CTE).
func TestBuild_SpanList_TraceFilter_Variables(t *testing.T) {
b := newTestBuilder(t)
build := func(expr string, vars map[string]qbtypes.VariableItem) (*qbtypes.Statement, error) {
return b.Build(context.Background(), valuer.UUID{}, testStartMs, testEndMs, qbtypes.RequestTypeRaw,
qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Signal: telemetrytypes.SignalTraces,
Filter: &qbtypes.Filter{Expression: expr},
Limit: 10,
}, vars)
}
stmt, err := build("trace.output_tokens > $threshold",
map[string]qbtypes.VariableItem{"threshold": {Value: 700}})
require.NoError(t, err)
require.Contains(t, stmt.Query, "HAVING output_tokens > 700")
stmt, err = build("trace.output_tokens > $threshold",
map[string]qbtypes.VariableItem{"threshold": {Type: qbtypes.DynamicVariableType, Value: "__all__"}})
require.NoError(t, err)
require.NotContains(t, stmt.Query, "__trace_scope")
}

View File

@@ -31,15 +31,24 @@ var (
// (e.g. gen_ai spans); the TraceScope decides which spans are in scope and which
// per-trace columns to compute.
type scopedTraceStatementBuilder struct {
logger *slog.Logger
metadataStore telemetrytypes.MetadataStore
fm qbtypes.FieldMapper
cb qbtypes.ConditionBuilder
scope TraceScope
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
logger *slog.Logger
metadataStore telemetrytypes.MetadataStore
fm qbtypes.FieldMapper
cb qbtypes.ConditionBuilder
scope TraceScope
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
// scopedDelegate is traceStmtBuilder's trace-scoping capability, resolved once at
// construction; nil when the delegate cannot constrain a query by trace ids.
scopedDelegate traceScopedStatementBuilder
resourceFilterStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
}
// traceScopedStatementBuilder is the delegate's optional capability of constraining a
// query to a set of trace ids (implemented by the traces statement builder).
type traceScopedStatementBuilder interface {
BuildTraceScoped(ctx context.Context, orgID valuer.UUID, start, end uint64, requestType qbtypes.RequestType, query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation], variables map[string]qbtypes.VariableItem, traceScope *qbtypes.Statement) (*qbtypes.Statement, error)
}
var _ qbtypes.StatementBuilder[qbtypes.TraceAggregation] = (*scopedTraceStatementBuilder)(nil)
// NewFactory returns a provider factory for a scoped trace statement builder. The
@@ -90,6 +99,7 @@ func NewScopedTraceStatementBuilder(
fl,
)
scopedDelegate, _ := traceStmtBuilder.(traceScopedStatementBuilder)
return &scopedTraceStatementBuilder{
logger: scopedSettings.Logger(),
metadataStore: metadataStore,
@@ -97,6 +107,7 @@ func NewScopedTraceStatementBuilder(
cb: conditionBuilder,
scope: scope,
traceStmtBuilder: traceStmtBuilder,
scopedDelegate: scopedDelegate,
resourceFilterStmtBuilder: resourceFilterStmtBuilder,
}
}
@@ -114,14 +125,18 @@ func (b *scopedTraceStatementBuilder) Build(
case qbtypes.RequestTypeTrace:
return b.buildTraceListQuery(ctx, orgID, querybuilder.ToNanoSecs(start), querybuilder.ToNanoSecs(end), query, variables)
case qbtypes.RequestTypeRaw:
if err := b.validateRawOrderKeys(query); err != nil {
return nil, err
}
return b.buildDelegated(ctx, orgID, start, end, requestType, query, variables)
default:
return nil, ErrUnsupportedRequestType
}
}
// buildDelegated ANDs the base gate into the user filter and delegates to the
// standard trace builder (the span-list / raw path).
// buildDelegated serves the span-list / raw path: the gate is ANDed into the filter's
// span-level part and the query delegates to the standard trace builder; a trace-level
// part becomes a qualification the delegate constrains trace_id by.
func (b *scopedTraceStatementBuilder) buildDelegated(
ctx context.Context,
orgID valuer.UUID,
@@ -130,17 +145,127 @@ func (b *scopedTraceStatementBuilder) buildDelegated(
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
variables map[string]qbtypes.VariableItem,
) (*qbtypes.Statement, error) {
var spanExpr, traceExpr string
if query.Filter != nil && strings.TrimSpace(query.Filter.Expression) != "" {
var err error
spanExpr, traceExpr, err = querybuilder.SplitFilterForAggregates(query.Filter.Expression, b.aggregateAliasSet())
if err != nil {
return nil, err
}
}
traceExpr, err := substituteTraceLevelVariables(traceExpr, variables)
if err != nil {
return nil, err
}
gate := b.scope.FilterExpression
expr := gate
if query.Filter != nil && strings.TrimSpace(query.Filter.Expression) != "" {
expr = fmt.Sprintf("(%s) AND (%s)", gate, query.Filter.Expression)
if strings.TrimSpace(spanExpr) != "" {
expr = fmt.Sprintf("(%s) AND (%s)", gate, spanExpr)
}
// shallow copy; only Filter is replaced, caller's query untouched
gated := query
gated.Filter = &qbtypes.Filter{Expression: expr}
return b.traceStmtBuilder.Build(ctx, orgID, start, end, requestType, gated, variables)
if strings.TrimSpace(traceExpr) == "" {
return b.traceStmtBuilder.Build(ctx, orgID, start, end, requestType, gated, variables)
}
if b.scopedDelegate == nil {
return nil, errors.NewInternalf(errors.CodeInternal, "trace statement builder does not support trace-scoped queries")
}
scope, err := b.buildTraceScopeStatement(ctx, orgID, querybuilder.ToNanoSecs(start), querybuilder.ToNanoSecs(end), traceExpr)
if err != nil {
return nil, err
}
return b.scopedDelegate.BuildTraceScoped(ctx, orgID, start, end, requestType, gated, variables, scope)
}
// validateRawOrderKeys rejects ordering the span list by an explicitly trace-level
// aggregate (trace. prefix or trace field context) — the per-trace value does not
// exist on span rows. Bare names pass through: they may legitimately be span columns
// (duration_nano, timestamp).
func (b *scopedTraceStatementBuilder) validateRawOrderKeys(query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]) error {
aliases := b.aggregateAliasSet()
for _, o := range query.Order {
key := telemetrytypes.GetFieldKeyFromKeyText(o.Key.Name)
if _, ok := aliases[key.Name]; !ok {
continue
}
if key.FieldContext == telemetrytypes.FieldContextTrace || o.Key.FieldContext == telemetrytypes.FieldContextTrace {
return errors.NewInvalidInputf(errors.CodeInvalidInput,
"ordering the span list by trace-level aggregate %q is not supported; order by span columns instead (e.g. timestamp, duration_nano)", o.Key.Name)
}
}
return nil
}
// substituteTraceLevelVariables resolves query variables in a trace-level expression.
// The span-level parts bind variables via PrepareWhereClause; trace-level parts are
// text rewrites, so variables become literals (a dynamic __all__ drops its condition).
func substituteTraceLevelVariables(expr string, variables map[string]qbtypes.VariableItem) (string, error) {
if strings.TrimSpace(expr) == "" || len(variables) == 0 {
return expr, nil
}
return qbvariables.ReplaceVariablesInExpression(expr, variables)
}
// buildTraceScopeStatement builds the __trace_scope statement: trace ids whose
// window-clipped per-trace aggregates satisfy traceExpr. The same scan as the matched
// CTE, minus its span-filter widening, resource prune, ordering and pagination.
// start/end are ns.
func (b *scopedTraceStatementBuilder) buildTraceScopeStatement(ctx context.Context, orgID valuer.UUID, start, end uint64, traceExpr string) (*qbtypes.Statement, error) {
keys, err := b.fetchKeys(ctx, orgID)
if err != nil {
return nil, err
}
sb := sqlbuilder.NewSelectBuilder()
maskExpr, resolved, err := b.resolveFor(ctx, orgID, start, end, keys, sb)
if err != nil {
return nil, err
}
orderableSet := orderableAliasSet(resolved)
if err := validateAggregateFilter(traceExpr, orderableSet); err != nil {
return nil, err
}
needed := neededMatchedAliases(nil, traceExpr, orderableSet)
selects := []string{"trace_id"}
for _, rc := range resolved {
if _, ok := needed[rc.alias]; !ok {
continue
}
selects = append(selects, rc.expr+" AS "+quoteAlias(rc.alias))
}
sb.Select(selects...)
sb.From(fmt.Sprintf("%s.%s", tracestelemetryschema.DBName, tracestelemetryschema.SpanIndexV3TableName))
sb.Where(
sb.GE("timestamp", fmt.Sprintf("%d", start)),
sb.L("timestamp", fmt.Sprintf("%d", end)),
sb.GE("ts_bucket_start", start/querybuilder.NsToSeconds-querybuilder.BucketAdjustment),
sb.LE("ts_bucket_start", end/querybuilder.NsToSeconds),
"("+maskExpr+")",
)
sb.GroupBy("trace_id")
// the rewriter matches raw key text, so map the trace. form alongside the bare name
columnMap := make(map[string]string, len(orderableSet)*2)
for a := range orderableSet {
columnMap[a] = quoteAlias(a)
columnMap[telemetrytypes.FieldContextTrace.StringValue()+"."+a] = quoteAlias(a)
}
hv, err := querybuilder.NewHavingExpressionRewriter().Rewrite(traceExpr, columnMap)
if err != nil {
return nil, err
}
if hv != "" {
// escape user text so a literal $ isn't read as an arg marker
sb.Having(sqlbuilder.Escape(hv))
}
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return &qbtypes.Statement{Query: sql, Args: args}, nil
}
// buildTraceListQuery wires the CTE pipeline (start/end are nanoseconds):

View File

@@ -32,6 +32,9 @@ type traceQueryStatementBuilder struct {
resourceFilterResolver *resourcefilter.ResourceFingerprintResolver[qbtypes.TraceAggregation]
aggExprRewriter qbtypes.AggExprRewriter
skipResourceFingerprintEnabled bool
// traceScope is set only on the per-call copy made by BuildTraceScoped; it
// constrains the query to trace ids selected by the __trace_scope CTE.
traceScope *qbtypes.Statement
}
var _ qbtypes.StatementBuilder[qbtypes.TraceAggregation] = (*traceQueryStatementBuilder)(nil)
@@ -95,6 +98,38 @@ func NewTraceQueryStatementBuilder(
}
}
// BuildTraceScoped is Build additionally constrained to spans whose trace_id is
// selected by traceScope. The receiver is copied so the shared builder stays stateless.
func (b *traceQueryStatementBuilder) BuildTraceScoped(
ctx context.Context,
orgID valuer.UUID,
start uint64,
end uint64,
requestType qbtypes.RequestType,
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
variables map[string]qbtypes.VariableItem,
traceScope *qbtypes.Statement,
) (*qbtypes.Statement, error) {
// The scope is wired into the list query only; reject other request types rather
// than silently dropping the constraint.
if requestType != qbtypes.RequestTypeRaw {
return nil, errors.NewInternalf(errors.CodeInternal, "trace-scoped build supports only the raw request type, got %s", requestType.StringValue())
}
scoped := *b
scoped.traceScope = traceScope
return scoped.Build(ctx, orgID, start, end, requestType, query, variables)
}
// attachTraceScope adds the trace-scope condition to sb and returns the CTE fragment
// + args to prepend; both empty when no scope is set.
func (b *traceQueryStatementBuilder) attachTraceScope(sb *sqlbuilder.SelectBuilder) (string, []any) {
if b.traceScope == nil {
return "", nil
}
sb.Where("trace_id GLOBAL IN (SELECT trace_id FROM __trace_scope)")
return fmt.Sprintf("__trace_scope AS (%s)", b.traceScope.Query), b.traceScope.Args
}
// Build builds a SQL query for traces based on the given parameters.
func (b *traceQueryStatementBuilder) Build(
ctx context.Context,
@@ -318,6 +353,11 @@ func (b *traceQueryStatementBuilder) buildListQuery(
cteArgs = append(cteArgs, args)
}
if scopeFrag, scopeArgs := b.attachTraceScope(sb); scopeFrag != "" {
cteFragments = append(cteFragments, scopeFrag)
cteArgs = append(cteArgs, scopeArgs)
}
for i, field := range query.SelectFields {
expr, err := b.fm.ColumnExpressionFor(ctx, orgID, start, end, &field, telemetrytypes.FieldDataTypeUnspecified, keys)
if err != nil {

View File

@@ -72,6 +72,14 @@ func (c *Claims) LogValue() slog.Value {
)
}
func (c *Claims) IsSelfAccess(id string) error {
if c.UserID == id {
return nil
}
return errors.NewForbiddenf(errors.CodeForbidden, "only the user/admin can access their own resource")
}
func (c *Claims) IdentityID() string {
if c.Principal == PrincipalUser {
return c.UserID

View File

@@ -65,7 +65,7 @@ var (
ResourceMetaResourceTTLSetting = NewResourceMetaResource(KindTTLSetting)
ResourceMetaResourceRule = NewResourceMetaResource(KindRule)
ResourceMetaResourcePlannedMaintenance = NewResourceMetaResource(KindPlannedMaintenance)
ResourceMetaResourceSavedView = NewResourceMetaResource(KindSavedView, VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete)
ResourceMetaResourceSavedView = NewResourceMetaResource(KindSavedView)
ResourceMetaResourceTraceFunnel = NewResourceMetaResource(KindTraceFunnel)
ResourceMetaResourceFactorPassword = NewResourceMetaResource(KindFactorPassword)
ResourceMetaResourceFactorAPIKey = NewResourceMetaResource(KindFactorAPIKey, VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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