Compare commits

..

11 Commits

Author SHA1 Message Date
makeavish
6e668fd9ee feat(empty-state): add org context API for contextual empty states
Adds GET /api/v1/empty_state/org_context returning raw org-level
observability signals (ingestion presence per signal, infra metrics,
alert/dashboard/saved-view counts, firing and recently-fired alerts,
raw license state) consumed by the AI assistant to render contextual
empty-state chips. Includes unit tests and an integration test suite.
2026-06-10 23:15:41 +05:30
primus-bot[bot]
c0c9039428 chore(release): bump to v0.128.0 (#11633)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-06-10 06:59:07 +00:00
Nikhil Soni
a014e9c0cb chore(trace-detail): waterfall v3 and v2 cleanup (#11609)
* chore: cleanup waterfall v3 api

We've moved to v4 as part of memory allocation optimisation

* chore: remove select all limit from waterfall request

It was added for testing only and fine tuning the limit value

* chore: update openapi specs

* chore: remove waterfall v2 since we've moved to v4 now
2026-06-10 05:15:36 +00:00
Vinicius Lourenço
b898269ddc chore(api): deprecate any hand-written api call (#11632) 2026-06-10 01:44:03 +00:00
swapnil-signoz
8fdad21a2e chore: bumping integration agent version to v0.0.11 (#11621)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-06-09 22:40:30 +00:00
Nikhil Mantri
2e0d25479a feat(infra-monitoring): v2 clusters integration tests (#11430)
* chore: updated logic and use centralized function in the module

* chore: filter metric groups

* chore: filter metric groups

* chore: formula correction

* chore: added step flooring note

* chore: comment correction

* chore: comment correction

* chore: removed function

* chore: renamed variables

* chore: added happy test

* chore: added test 2 for accuracy and test 3 for missing metrics check

* chore: added filter test 4

* chore: added 5th test for filterByStatus

* chore: added group by tests

* chore: pagination test added

* chore: added validation tests

* chore: added auth test

* chore: added all tests

* chore: fix for surfacing meta for pods custom group by

* chore: added nodes integration test suite

* chore: namespaces integration tests

* ci: register inframonitoring suite + ruff format 01_hosts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: added integration tests for clusters

* chore: formatting changed

* chore: formatting changed

* chore: formatting changed

* chore: added order by host.name test

* refactor(infra-monitoring): address review comments on hosts integration tests

- inline _post helper at call sites
- combine filter operator tests into parametrized test_hosts_filter
- combine bad attr/grammar tests into parametrized test_hosts_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_hosts_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align pods integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_pods_filter
- combine bad attr/grammar tests into parametrized test_pods_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_pods_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align nodes integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_nodes_filter
- combine bad attr/grammar tests into parametrized test_nodes_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_nodes_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align namespaces integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_namespaces_filter
- combine bad attr/grammar tests into parametrized test_namespaces_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_namespaces_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align clusters integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_clusters_filter
- combine bad attr/grammar tests into parametrized test_clusters_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_clusters_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts groupby tests into parametrized test

- merge test_hosts_groupby_hostname + test_hosts_groupby_os_type into
  test_hosts_groupby parametrized on (dataset, group key, expected counts/values)
- preserves all assertions incl hostName populated-vs-empty branch coverage

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts orderby tests into parametrized test

- merge total_invariant_across_orderby + orderby_correctness + orderby_by_host_name
  into test_hosts_orderby parametrized on (column, record_field) x direction
- each case asserts both the total/len invariant and sortedness; sortedness now
  covered for all metric columns, not just cpu
- single dataset (hosts_orderby.jsonl) + CONTAINS 'order-' guard on all cases

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts pagination tests

- fold offset-beyond-total case into the test_hosts_pagination page walk
  (offset K+5 expects 0 records via max(0, min(limit, K-offset)); total
  invariant covers the beyond-total page's total == K)
- single seed instead of two

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): merge hosts happy_path into test_hosts_accuracy

- happy_path and value_accuracy datasets were structurally identical
  (same 4 metrics, same sample counts, 2 hosts); one test now asserts
  shape/contract + exact metric values in a single seed/request
- drop unused hosts_happy_path.jsonl

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine pods integration tests logically

- merge happy_path into test_pods_accuracy (datasets structurally identical);
  drop unused pods_happy_path.jsonl
- merge groupby namespace/deployment into parametrized test_pods_groupby
- merge orderby invariant + correctness + by-pod-name into test_pods_orderby
  ((column, record_field) x direction; sortedness now covered for all columns)
- fold offset-beyond-total into test_pods_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine nodes integration tests logically

- merge happy_path into test_nodes_accuracy; drop unused nodes_happy_path.jsonl
- merge orderby invariant + correctness + by-node-name into test_nodes_orderby
  ((column, record_field) x direction; sortedness now covered for all columns)
- fold offset-beyond-total into test_nodes_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine namespaces integration tests logically

- merge happy_path into test_namespaces_accuracy; drop unused namespaces_happy_path.jsonl
- merge orderby invariant + correctness + by-namespace-name into test_namespaces_orderby
- fold offset-beyond-total into test_namespaces_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine clusters integration tests logically

- merge happy_path into test_clusters_accuracy; drop unused clusters_happy_path.jsonl
- merge orderby invariant + correctness + by-cluster-name into test_clusters_orderby
- fold offset-beyond-total into test_clusters_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in hosts filter tests

- regenerate hosts_filter_dataset.jsonl so every host mirrors the acc-h1
  sample pattern from hosts_value_accuracy.jsonl (CI-proven expected values)
- test_hosts_filter now asserts cpu/memory/wait/load15/diskUsage per filtered
  record against FILTER_DATASET_EXPECTED (1e-9), proving filters don't
  distort aggregation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): scope filter expected values inside test_hosts_filter

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in pods filter tests

- regenerate pods_filter_dataset.jsonl so every pod mirrors the acc-p1
  sample pattern from pods_value_accuracy.jsonl (CI-proven expected values)
- test_pods_filter now asserts the 6 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in nodes filter tests

- regenerate nodes_filter_dataset.jsonl so every node mirrors the acc-n1
  sample pattern from nodes_value_accuracy.jsonl (CI-proven expected values)
- test_nodes_filter now asserts the 4 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in namespaces filter tests

- regenerate namespaces_filter_dataset.jsonl so every namespace mirrors the
  acc-ns-1 sample pattern (2 pods) from namespaces_value_accuracy.jsonl
- test_namespaces_filter now asserts namespaceCPU/namespaceMemory per record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in clusters filter tests

- regenerate clusters_filter_dataset.jsonl so every cluster mirrors the
  acc-cluster-1 sample pattern (2 nodes) from clusters_value_accuracy.jsonl
- test_clusters_filter now asserts the 4 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align nodes groupby + pod-phase tests with structure

- merge pod_phase_counts_list_mode + _no_pods_on_node into parametrized
  test_nodes_pod_phase_counts ((dataset, node, filter, expected) cases)
- rename groupby_cluster -> test_nodes_groupby, parametrize over
  k8s.node.name + k8s.cluster.name; adds the node-name-in-groupBy branch
  (nodeName populated, condition derived) that hosts/pods both cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align namespaces groupby with structure

- rename groupby_cluster -> test_namespaces_groupby, parametrize over
  k8s.namespace.name + k8s.cluster.name; adds the namespace-name-in-groupBy
  branch (namespaceName populated) that hosts/pods/nodes all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align clusters groupby with structure

- rename groupby_cloud_provider -> test_clusters_groupby, parametrize over
  k8s.cluster.name + cloud.provider; adds the cluster-name-in-groupBy branch
  (clusterName populated) that hosts/pods/nodes/namespaces all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 18:51:23 +00:00
swapnil-signoz
73c2c15200 feat: adding support for VMs in Azure integration (#11573)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat: adding supoprt for virtual machines to azure one click integration

* chore: generating openapi spec

* chore: updating dashboard title
2026-06-09 15:12:57 +00:00
Aditya Singh
34203c781f feat(traces): Noz button on trace detail page (#11624)
* feat: add noz ai button

* feat: badge color fix

* feat: revert badge change
2026-06-09 14:34:43 +00:00
swapnil-signoz
1ba9b90855 feat: adding assets and dashboards for azure app services (#11565)
* feat: adding assets and dashboards for azure app services

* refactor: updating metric names

* chore: openapi spec changes

* chore: generating frontend types
2026-06-09 14:30:53 +00:00
SagarRajput-7
927951b67a fix(billing): fix cancel subscription flow failing silently for users without a default mail client (#11622)
* fix(billing): fix cancel subscription flow failing silently for users without a default mail client

* fix(billing): refactor test case

* fix(billing): added log event to the copy template and reopen button

* fix(billing): test case refactor

* fix(billing): use native <a> for mailto retry to maximize cross-browser reliability
2026-06-09 14:18:43 +00:00
Vinicius Lourenço
05ad8d113d chore(vite.config): ensure commits information for sentry (#11623)
* chore(vite.config): ensure commits information is added on new release

* ci(build-staging): enable sentry on staging envs
2026-06-09 13:54:19 +00:00
118 changed files with 8603 additions and 117 deletions

View File

@@ -64,6 +64,10 @@ jobs:
run: |
mkdir -p frontend
echo 'CI=1' > frontend/.env
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env
echo 'VITE_SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
echo 'VITE_TUNNEL_URL="${{ secrets.NP_TUNNEL_URL }}"' >> frontend/.env
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'VITE_PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env

View File

@@ -43,6 +43,7 @@ jobs:
- callbackauthn
- cloudintegrations
- dashboard
- emptystate
- ingestionkeys
- inframonitoring
- logspipelines

2
.gitignore vendored
View File

@@ -40,6 +40,8 @@ frontend/src/constants/env.ts
**/__debug_bin
.env
# sqlite db created at repo root by `make go-run-community` / `make go-run-enterprise`
/signoz.db
pkg/query-service/signoz.db
pkg/query-service/tests/test-deploy/data/

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.127.1
image: signoz/signoz:v0.128.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.127.1
image: signoz/signoz:v0.128.0
ports:
- "8080:8080" # signoz port
volumes:

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.127.1}
image: signoz/signoz:${VERSION:-v0.128.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.127.1}
image: signoz/signoz:${VERSION:-v0.128.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -1360,6 +1360,8 @@ components:
- sqs
- storageaccountsblob
- cdnprofile
- virtualmachine
- appservice
- containerapp
- aks
type: string
@@ -3353,6 +3355,58 @@ components:
- kind
- spec
type: object
EmptystatetypesOrgContext:
properties:
activeFiringAlertsCount:
type: integer
alertsCount:
type: integer
dashboardsCount:
type: integer
hasInfraMetrics:
type: boolean
hasIngestedData:
type: boolean
ingestingCurrently:
type: boolean
licenseStatus:
description: Raw Zeus license state. Known values include DEFAULTED, ACTIVATED,
EXPIRED, ISSUED, EVALUATING, EVALUATION_EXPIRED, TERMINATED, CANCELLED.
UNKNOWN is emitted when no license state is available.
type: string
recentlyFiredAlertsCount:
type: integer
savedViewsCount:
type: integer
signalsIngested:
$ref: '#/components/schemas/EmptystatetypesSignalsIngested'
required:
- hasIngestedData
- signalsIngested
- ingestingCurrently
- hasInfraMetrics
- alertsCount
- activeFiringAlertsCount
- recentlyFiredAlertsCount
- dashboardsCount
- savedViewsCount
- licenseStatus
type: object
EmptystatetypesSignalsIngested:
properties:
logs:
type: boolean
metrics:
description: Excludes span-generated metrics (signoz_ prefix), which only
prove traces ingestion.
type: boolean
traces:
type: boolean
required:
- logs
- traces
- metrics
type: object
ErrorsJSON:
properties:
code:
@@ -9507,6 +9561,53 @@ paths:
summary: Update downtime schedule
tags:
- downtimeschedules
/api/v1/empty_state/org_context:
get:
deprecated: false
description: This endpoint returns raw org-level observability signals used
to render contextual empty states
operationId: GetOrgContext
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/EmptystatetypesOrgContext'
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
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get org context for empty states
tags:
- emptystate
/api/v1/export_raw_data:
post:
deprecated: false

View File

@@ -5,6 +5,13 @@ import convertObjectIntoParams from 'lib/query/convertObjectIntoParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/getTriggered';
/**
* @deprecated Use the generated `useGetAlerts` hook (or `getAlerts` fetcher) from
* `api/generated/services/alerts` 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 getTriggered = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createEmail';
/**
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
* `api/generated/services/channels` 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 create = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createMsTeams';
/**
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
* `api/generated/services/channels` 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 create = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createOpsgenie';
/**
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
* `api/generated/services/channels` 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 create = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createPager';
/**
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
* `api/generated/services/channels` 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 create = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createSlack';
/**
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
* `api/generated/services/channels` 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 create = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createWebhook';
/**
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
* `api/generated/services/channels` 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 create = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/delete';
/**
* @deprecated Use the generated `useDeleteChannelByID` hook (or `deleteChannelByID` fetcher) from
* `api/generated/services/channels` 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 deleteChannel = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editEmail';
/**
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
* `api/generated/services/channels` 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 editEmail = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editMsTeams';
/**
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
* `api/generated/services/channels` 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 editMsTeams = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorResponse, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editOpsgenie';
/**
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
* `api/generated/services/channels` 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 editOpsgenie = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps> | ErrorResponse> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editPager';
/**
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
* `api/generated/services/channels` 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 editPager = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editSlack';
/**
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
* `api/generated/services/channels` 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 editSlack = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editWebhook';
/**
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
* `api/generated/services/channels` 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 editWebhook = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -5,6 +5,13 @@ import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/get';
import { Channels } from 'types/api/channels/getAll';
/**
* @deprecated Use the generated `useGetChannelByID` hook (or `getChannelByID` fetcher) from
* `api/generated/services/channels` 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 get = async (props: Props): Promise<SuccessResponseV2<Channels>> => {
try {
const response = await axios.get<PayloadProps>(`/channels/${props.id}`);

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Channels, PayloadProps } from 'types/api/channels/getAll';
/**
* @deprecated Use the generated `useListChannels` hook (or `listChannels` fetcher) from
* `api/generated/services/channels` 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 getAll = async (): Promise<SuccessResponseV2<Channels[]>> => {
try {
const response = await axios.get<PayloadProps>('/channels');

View File

@@ -5,6 +5,13 @@ import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constan
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { CreatePublicDashboardProps } from 'types/api/dashboard/public/create';
/**
* @deprecated Use the generated `useCreatePublicDashboard` hook (or `createPublicDashboard` fetcher) from
* `api/generated/services/dashboard` 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 createPublicDashboard = async (
props: CreatePublicDashboardProps,
): Promise<SuccessResponseV2<CreatePublicDashboardProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardDataProps, PayloadProps,PublicDashboardDataProps } from 'types/api/dashboard/public/get';
/**
* @deprecated Use the generated `useGetPublicDashboardData` hook (or `getPublicDashboardData` fetcher) from
* `api/generated/services/dashboard` 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 getPublicDashboardData = async (props: GetPublicDashboardDataProps): Promise<SuccessResponseV2<PublicDashboardDataProps>> => {
try {
const response = await axios.get<PayloadProps>(`/public/dashboards/${props.id}`);

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardMetaProps, PayloadProps,PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
/**
* @deprecated Use the generated `useGetPublicDashboard` hook (or `getPublicDashboard` fetcher) from
* `api/generated/services/dashboard` 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 getPublicDashboardMeta = async (props: GetPublicDashboardMetaProps): Promise<SuccessResponseV2<PublicDashboardMetaProps>> => {
try {
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}/public`);

View File

@@ -6,6 +6,13 @@ import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardWidgetDataProps } from 'types/api/dashboard/public/getWidgetData';
/**
* @deprecated Use the generated `useGetPublicDashboardWidgetQueryRange` hook (or `getPublicDashboardWidgetQueryRange` fetcher) from
* `api/generated/services/dashboard` 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 getPublicDashboardWidgetData = async (props: GetPublicDashboardWidgetDataProps): Promise<SuccessResponseV2<MetricRangePayloadV5>> => {
try {
const response = await axios.get(`/public/dashboards/${props.id}/widgets/${props.index}/query_range`, {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps,RevokePublicDashboardAccessProps } from 'types/api/dashboard/public/delete';
/**
* @deprecated Use the generated `useDeletePublicDashboard` hook (or `deletePublicDashboard` fetcher) from
* `api/generated/services/dashboard` 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 revokePublicDashboardAccess = async (
props: RevokePublicDashboardAccessProps,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -5,6 +5,13 @@ import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constan
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { UpdatePublicDashboardProps } from 'types/api/dashboard/public/update';
/**
* @deprecated Use the generated `useUpdatePublicDashboard` hook (or `updatePublicDashboard` fetcher) from
* `api/generated/services/dashboard` 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 updatePublicDashboard = async (
props: UpdatePublicDashboardProps,
): Promise<SuccessResponseV2<UpdatePublicDashboardProps>> => {

View File

@@ -9,6 +9,13 @@ interface ISubstituteVars {
compositeQuery: ICompositeMetricQuery;
}
/**
* @deprecated Use the generated `useReplaceVariables` hook (or `replaceVariables` fetcher) from
* `api/generated/services/querier` 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.
*/
export const getSubstituteVars = async (
props?: Partial<QueryRangePayloadV5>,
signal?: AbortSignal,

View File

@@ -8,6 +8,12 @@ import { FieldKeyResponse } from 'types/api/dynamicVariables/getFieldKeys';
* Get field keys for a given signal type
* @param signal Type of signal (traces, logs, metrics)
* @param name Optional search text
*
* @deprecated Use the generated `useGetFieldsKeys` hook (or `getFieldsKeys` fetcher) from
* `api/generated/services/fields` 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.
*/
export const getFieldKeys = async (
signal?: 'traces' | 'logs' | 'metrics',

View File

@@ -11,6 +11,12 @@ import { FieldValueResponse } from 'types/api/dynamicVariables/getFieldValues';
* @param name Name of the attribute for which values are being fetched
* @param value Optional search text
* @param existingQuery Optional existing query - across all present dynamic variables
*
* @deprecated Use the generated `useGetFieldsValues` hook (or `getFieldsValues` fetcher) from
* `api/generated/services/fields` 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.
*/
export const getFieldValues = async (
signal?: 'traces' | 'logs' | 'metrics',

View File

@@ -0,0 +1,107 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useQuery } from 'react-query';
import type {
InvalidateOptions,
QueryClient,
QueryFunction,
QueryKey,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
GetOrgContext200,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType } from '../../../generatedAPIInstance';
/**
* This endpoint returns raw org-level observability signals used to render contextual empty states
* @summary Get org context for empty states
*/
export const getOrgContext = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetOrgContext200>({
url: `/api/v1/empty_state/org_context`,
method: 'GET',
signal,
});
};
export const getGetOrgContextQueryKey = () => {
return [`/api/v1/empty_state/org_context`] as const;
};
export const getGetOrgContextQueryOptions = <
TData = Awaited<ReturnType<typeof getOrgContext>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getOrgContext>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetOrgContextQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getOrgContext>>> = ({
signal,
}) => getOrgContext(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getOrgContext>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetOrgContextQueryResult = NonNullable<
Awaited<ReturnType<typeof getOrgContext>>
>;
export type GetOrgContextQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get org context for empty states
*/
export function useGetOrgContext<
TData = Awaited<ReturnType<typeof getOrgContext>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getOrgContext>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetOrgContextQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get org context for empty states
*/
export const invalidateGetOrgContext = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetOrgContextQueryKey() },
options,
);
return queryClient;
};

View File

@@ -2651,6 +2651,8 @@ export enum CloudintegrationtypesServiceIDDTO {
sqs = 'sqs',
storageaccountsblob = 'storageaccountsblob',
cdnprofile = 'cdnprofile',
virtualmachine = 'virtualmachine',
appservice = 'appservice',
containerapp = 'containerapp',
aks = 'aks',
}
@@ -4824,6 +4826,63 @@ export enum DashboardtypesVariablePluginKindDTO {
'signoz/QueryVariable' = 'signoz/QueryVariable',
'signoz/CustomVariable' = 'signoz/CustomVariable',
}
export interface EmptystatetypesSignalsIngestedDTO {
/**
* @type boolean
*/
logs: boolean;
/**
* @type boolean
* @description Excludes span-generated metrics (signoz_ prefix), which only prove traces ingestion.
*/
metrics: boolean;
/**
* @type boolean
*/
traces: boolean;
}
export interface EmptystatetypesOrgContextDTO {
/**
* @type integer
*/
activeFiringAlertsCount: number;
/**
* @type integer
*/
alertsCount: number;
/**
* @type integer
*/
dashboardsCount: number;
/**
* @type boolean
*/
hasInfraMetrics: boolean;
/**
* @type boolean
*/
hasIngestedData: boolean;
/**
* @type boolean
*/
ingestingCurrently: boolean;
/**
* @type string
* @description Raw Zeus license state. Known values include DEFAULTED, ACTIVATED, EXPIRED, ISSUED, EVALUATING, EVALUATION_EXPIRED, TERMINATED, CANCELLED. UNKNOWN is emitted when no license state is available.
*/
licenseStatus: string;
/**
* @type integer
*/
recentlyFiredAlertsCount: number;
/**
* @type integer
*/
savedViewsCount: number;
signalsIngested: EmptystatetypesSignalsIngestedDTO;
}
export type FactoryResponseDTOServicesAnyOf = { [key: string]: string[] };
/**
@@ -9040,6 +9099,14 @@ export type GetDowntimeScheduleByID200 = {
export type UpdateDowntimeScheduleByIDPathParameters = {
id: string;
};
export type GetOrgContext200 = {
data: EmptystatetypesOrgContextDTO;
/**
* @type string
*/
status: string;
};
export type HandleExportRawDataPOSTParams = {
/**
* @enum csv,jsonl

View File

@@ -5,6 +5,13 @@ import {
QueryKeySuggestionsResponseProps,
} from 'types/api/querySuggestions/types';
/**
* @deprecated Use the generated `useGetFieldsKeys` hook (or `getFieldsKeys` fetcher) from
* `api/generated/services/fields` 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.
*/
export const getKeySuggestions = (
props: QueryKeyRequestProps,
): Promise<AxiosResponse<QueryKeySuggestionsResponseProps>> => {

View File

@@ -5,6 +5,13 @@ import {
QueryKeyValueSuggestionsResponseProps,
} from 'types/api/querySuggestions/types';
/**
* @deprecated Use the generated `useGetFieldsValues` hook (or `getFieldsValues` fetcher) from
* `api/generated/services/fields` 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.
*/
export const getValueSuggestions = (
props: QueryKeyValueRequestProps,
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {

View File

@@ -15,6 +15,13 @@ export interface CreateRoutingPolicyResponse {
message: string;
}
/**
* @deprecated Use the generated `useCreateRoutePolicy` hook (or `createRoutePolicy` fetcher) from
* `api/generated/services/routepolicies` 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 createRoutingPolicy = async (
props: CreateRoutingPolicyBody,
): Promise<

View File

@@ -8,6 +8,13 @@ export interface DeleteRoutingPolicyResponse {
message: string;
}
/**
* @deprecated Use the generated `useDeleteRoutePolicyByID` hook (or `deleteRoutePolicyByID` fetcher) from
* `api/generated/services/routepolicies` 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 deleteRoutingPolicy = async (
routingPolicyId: string,
): Promise<

View File

@@ -20,6 +20,13 @@ export interface GetRoutingPoliciesResponse {
data?: ApiRoutingPolicy[];
}
/**
* @deprecated Use the generated `useGetAllRoutePolicies` hook (or `getAllRoutePolicies` fetcher) from
* `api/generated/services/routepolicies` 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.
*/
export const getRoutingPolicies = async (
signal?: AbortSignal,
headers?: Record<string, string>,

View File

@@ -15,6 +15,13 @@ export interface UpdateRoutingPolicyResponse {
message: string;
}
/**
* @deprecated Use the generated `useUpdateRoutePolicy` hook (or `updateRoutePolicy` fetcher) from
* `api/generated/services/routepolicies` 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 updateRoutingPolicy = async (
id: string,
props: UpdateRoutingPolicyBody,

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/user/resetPassword';
/**
* @deprecated Use the generated `useResetPassword` hook (or `resetPassword` 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 resetPassword = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ 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>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/user/setInvite';
/**
* @deprecated Use the generated `useCreateInvite` hook (or `createInvite` 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 sendInvite = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -5,6 +5,13 @@ import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/preferences/list';
import { OrgPreference } from 'types/api/preferences/preference';
/**
* @deprecated Use the generated `useListOrgPreferences` hook (or `listOrgPreferences` fetcher) from
* `api/generated/services/preferences` 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 listPreference = async (): Promise<
SuccessResponseV2<OrgPreference[]>
> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Props } from 'types/api/preferences/update';
/**
* @deprecated Use the generated `useUpdateOrgPreference` hook (or `updateOrgPreference` fetcher) from
* `api/generated/services/preferences` 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 update = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put(`/org/preferences/${props.name}`, {

View File

@@ -5,6 +5,13 @@ import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/preferences/list';
import { UserPreference } from 'types/api/preferences/preference';
/**
* @deprecated Use the generated `useListUserPreferences` hook (or `listUserPreferences` fetcher) from
* `api/generated/services/preferences` 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 list = async (): Promise<SuccessResponseV2<UserPreference[]>> => {
try {
const response = await axios.get<PayloadProps>(`/user/preferences`);

View File

@@ -5,6 +5,13 @@ import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/preferences/get';
import { UserPreference } from 'types/api/preferences/preference';
/**
* @deprecated Use the generated `useGetUserPreference` hook (or `getUserPreference` fetcher) from
* `api/generated/services/preferences` 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 get = async (
props: Props,
): Promise<SuccessResponseV2<UserPreference>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Props } from 'types/api/preferences/update';
/**
* @deprecated Use the generated `useUpdateUserPreference` hook (or `updateUserPreference` fetcher) from
* `api/generated/services/preferences` 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 update = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put(`/user/preferences/${props.name}`, {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
import { Props, SessionsContext } from 'types/api/v2/sessions/context/get';
/**
* @deprecated Use the generated `useGetSessionContext` hook (or `getSessionContext` fetcher) from
* `api/generated/services/sessions` 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 get = async (
props: Props,
): Promise<SuccessResponseV2<SessionsContext>> => {

View File

@@ -3,6 +3,13 @@ import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
/**
* @deprecated Use the generated `useDeleteSession` hook (or `deleteSession` fetcher) from
* `api/generated/services/sessions` 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 deleteSession = async (): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.delete<RawSuccessResponse<null>>('/sessions');

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
import { Props, Token } from 'types/api/v2/sessions/email_password/post';
/**
* @deprecated Use the generated `useCreateSessionByEmailPassword` hook (or `createSessionByEmailPassword` fetcher) from
* `api/generated/services/sessions` 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 post = async (props: Props): Promise<SuccessResponseV2<Token>> => {
try {
const response = await axios.post<RawSuccessResponse<Token>>(

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
import { Props, Token } from 'types/api/v2/sessions/rotate/post';
/**
* @deprecated Use the generated `useRotateSession` hook (or `rotateSession` fetcher) from
* `api/generated/services/sessions` 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 post = async (props: Props): Promise<SuccessResponseV2<Token>> => {
try {
const response = await axios.post<RawSuccessResponse<Token>>(

View File

@@ -8,6 +8,13 @@ import {
QueryRangePayloadV5,
} from 'types/api/v5/queryRange';
/**
* @deprecated Use the generated `useQueryRangeV5` hook (or `queryRangeV5` fetcher) from
* `api/generated/services/querier` 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.
*/
export const getQueryRangeV5 = async (
props: QueryRangePayloadV5,
version: string,

View File

@@ -70,6 +70,7 @@ export const AIAssistantOpenSource = {
Icon: 'icon',
Shortcut: 'shortcut',
Cmdk: 'cmdk',
TraceDetails: 'trace_details',
} as const;
export type AIAssistantOpenSource =
(typeof AIAssistantOpenSource)[keyof typeof AIAssistantOpenSource];

View File

@@ -67,3 +67,40 @@
background: var(--secondary-background);
border: 1px solid var(--l1-border);
}
.fallbackBody {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
.fallbackHint {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
line-height: var(--paragraph-base-400-line-height);
color: var(--l2-foreground);
margin: 0;
}
.fallbackEmail {
font-size: var(--paragraph-base-500-font-size);
font-weight: var(--paragraph-base-500-font-weight);
color: var(--l1-foreground);
word-break: break-all;
}
.fallbackActions {
display: flex;
gap: var(--spacing-3);
flex-wrap: wrap;
padding-top: var(--padding-4);
}
.retryLink {
box-sizing: border-box;
text-decoration: none;
&:hover {
text-decoration: none;
}
}

View File

@@ -9,7 +9,37 @@ jest.mock('utils/basePath', () => ({
getBaseUrl: (): string => 'https://test.signoz.io',
}));
function mockMailto(): {
mockClick: jest.Mock;
appendSpy: jest.SpyInstance;
removeSpy: jest.SpyInstance;
} {
const mockClick = jest.fn();
const realCreateElement = document.createElement.bind(document);
// Create a real anchor so JSDOM's appendChild/removeChild accept it.
// Override its click() so no navigation occurs.
jest
.spyOn(document, 'createElement')
.mockImplementation((tag: string, options?: ElementCreationOptions) => {
if (tag === 'a') {
const anchor = realCreateElement('a') as HTMLAnchorElement;
anchor.click = mockClick;
return anchor;
}
return realCreateElement(tag, options);
});
const appendSpy = jest.spyOn(document.body, 'appendChild');
const removeSpy = jest.spyOn(document.body, 'removeChild');
return { mockClick, appendSpy, removeSpy };
}
describe('CancelSubscriptionBanner', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('renders banner with title and subtitle', () => {
render(<CancelSubscriptionBanner />);
expect(
@@ -35,12 +65,10 @@ describe('CancelSubscriptionBanner', () => {
screen.getByText(/Cancelling your subscription would stop your data/i),
).toBeInTheDocument();
expect(screen.getByText(/Type/i)).toBeInTheDocument();
expect(
screen.getByPlaceholderText(/Enter the word cancel/i),
).toBeInTheDocument();
expect(screen.getByTestId('cancel-confirm-input')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /go back/i })).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /cancel subscription/i }),
screen.getByTestId('cancel-subscription-confirm-btn'),
).toBeInTheDocument();
});
@@ -52,12 +80,10 @@ describe('CancelSubscriptionBanner', () => {
screen.getByRole('button', { name: /cancel subscription/i }),
);
const confirmButton = screen.getByRole('button', {
name: /cancel subscription/i,
});
const confirmButton = screen.getByTestId('cancel-subscription-confirm-btn');
expect(confirmButton).toBeDisabled();
const input = screen.getByPlaceholderText(/Enter the word cancel/i);
const input = screen.getByTestId('cancel-confirm-input');
await user.type(input, 'canc');
expect(confirmButton).toBeDisabled();
@@ -73,7 +99,7 @@ describe('CancelSubscriptionBanner', () => {
screen.getByRole('button', { name: /cancel subscription/i }),
);
const input = screen.getByPlaceholderText(/Enter the word cancel/i);
const input = screen.getByTestId('cancel-confirm-input');
await user.type(input, 'cancel');
await user.click(screen.getByRole('button', { name: /go back/i }));
@@ -84,19 +110,11 @@ describe('CancelSubscriptionBanner', () => {
await user.click(
screen.getByRole('button', { name: /cancel subscription/i }),
);
expect(screen.getByPlaceholderText(/Enter the word cancel/i)).toHaveValue('');
expect(screen.getByTestId('cancel-confirm-input')).toHaveValue('');
});
it('sends mailto to cloud-support with correct subject after typing "cancel"', async () => {
const realCreateElement = document.createElement.bind(document);
const mockClick = jest.fn();
const mockAnchor = { href: '', click: mockClick };
jest.spyOn(document, 'createElement').mockImplementation((tag: string) => {
if (tag === 'a') {
return mockAnchor as unknown as HTMLAnchorElement;
}
return realCreateElement(tag);
});
it('fires mailto via DOM-attached anchor and shows fallback view after confirming', async () => {
const { mockClick, appendSpy, removeSpy } = mockMailto();
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CancelSubscriptionBanner />);
@@ -104,18 +122,85 @@ describe('CancelSubscriptionBanner', () => {
await user.click(
screen.getByRole('button', { name: /cancel subscription/i }),
);
await user.type(screen.getByTestId('cancel-confirm-input'), 'cancel');
await user.click(screen.getByTestId('cancel-subscription-confirm-btn'));
const input = screen.getByPlaceholderText(/Enter the word cancel/i);
await user.type(input, 'cancel');
const appendedAnchor = appendSpy.mock.calls
.map(([node]) => node)
.find(
(node): node is HTMLAnchorElement =>
node instanceof HTMLAnchorElement && node.href.startsWith('mailto:'),
);
expect(appendedAnchor).toBeDefined();
expect(mockClick).toHaveBeenCalledTimes(1);
expect(removeSpy.mock.calls.some(([node]) => node === appendedAnchor)).toBe(
true,
);
expect(
screen.getByText(/An email draft has been opened/i),
).toBeInTheDocument();
expect(screen.getByText('cloud-support@signoz.io')).toBeInTheDocument();
expect(screen.getByTestId('copy-email-template-btn')).toBeInTheDocument();
expect(screen.getByTestId('retry-mailto-btn')).toBeInTheDocument();
});
it('copies email template to clipboard when Copy button is clicked', async () => {
mockMailto();
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CancelSubscriptionBanner />);
await user.click(
screen.getByRole('button', { name: /cancel subscription/i }),
);
await user.type(screen.getByTestId('cancel-confirm-input'), 'cancel');
await user.click(screen.getByTestId('cancel-subscription-confirm-btn'));
expect(mockAnchor.href).toContain('mailto:cloud-support@signoz.io');
expect(mockAnchor.href).toContain('Cancel%20My%20SigNoz%20Subscription');
expect(mockClick).toHaveBeenCalledTimes(1);
await user.click(screen.getByTestId('copy-email-template-btn'));
jest.restoreAllMocks();
await waitFor(() =>
expect(screen.getByTestId('copy-email-template-btn')).toHaveTextContent(
'Copied!',
),
);
});
it('retry link is a native anchor with correct mailto href in fallback view', async () => {
mockMailto();
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CancelSubscriptionBanner />);
await user.click(
screen.getByRole('button', { name: /cancel subscription/i }),
);
await user.type(screen.getByTestId('cancel-confirm-input'), 'cancel');
await user.click(screen.getByTestId('cancel-subscription-confirm-btn'));
const retryLink = screen.getByTestId('retry-mailto-btn');
expect(retryLink.tagName).toBe('A');
expect(retryLink).toHaveAttribute(
'href',
expect.stringContaining('mailto:cloud-support@signoz.io'),
);
});
it('closes fallback view when Close is clicked and resets state', async () => {
mockMailto();
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CancelSubscriptionBanner />);
await user.click(
screen.getByRole('button', { name: /cancel subscription/i }),
);
await user.type(screen.getByTestId('cancel-confirm-input'), 'cancel');
await user.click(screen.getByTestId('cancel-subscription-confirm-btn'));
await user.click(screen.getByRole('button', { name: /close/i }));
await waitFor(() =>
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(),
);
});
});

View File

@@ -1,27 +1,100 @@
import { useState } from 'react';
import { SolidInfoCircle, Undo2, X } from '@signozhq/icons';
import { useEffect, useRef, useState } from 'react';
import {
CircleCheck,
Copy,
MailOpen,
SolidInfoCircle,
Undo2,
X,
} from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { Input } from '@signozhq/ui/input';
import logEvent from 'api/common/logEvent';
import { pick } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { useCopyToClipboard } from 'react-use';
import { getBaseUrl } from 'utils/basePath';
import styles from './CancelSubscriptionBanner.module.scss';
import { Color } from '@signozhq/design-tokens';
import styles from './CancelSubscriptionBanner.module.scss';
const SUPPORT_EMAIL = 'cloud-support@signoz.io';
const MAX_MAILTO_URI_LENGTH = 1800;
type DialogView = 'confirm' | 'fallback';
function buildEmailBody(orgName: string, userEmail: string): string {
return [
'Hi SigNoz Team,',
'',
'I would like to cancel my SigNoz Cloud subscription.',
'Please find my account details below.',
'',
'Account Details:',
` • SigNoz URL: ${getBaseUrl()}`,
...(orgName ? [` • Organization: ${orgName}`] : []),
` • Account Email: ${userEmail}`,
'',
'Reason for Cancellation:',
'[Please share the reason for cancellation]',
'',
'Additional feedback (optional):',
'[Any other feedback]',
'',
'Regards,',
'[user name or team name]',
].join('\n');
}
function buildMailtoUri(orgName: string, userEmail: string): string {
const subject = encodeURIComponent('Cancel My SigNoz Subscription');
const body = encodeURIComponent(buildEmailBody(orgName, userEmail));
const full = `mailto:${SUPPORT_EMAIL}?subject=${subject}&body=${body}`;
if (full.length <= MAX_MAILTO_URI_LENGTH) {
return full;
}
const shortBody = encodeURIComponent(
'Hi SigNoz Team,\n\nI would like to cancel my SigNoz Cloud subscription.\nPlease find my account details and reason for cancellation below.\n\n[Your details here]\n\nRegards,',
);
return `mailto:${SUPPORT_EMAIL}?subject=${subject}&body=${shortBody}`;
}
function openMailto(uri: string): void {
const link = document.createElement('a');
link.href = uri;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function CancelSubscriptionBanner(): JSX.Element {
const [open, setOpen] = useState(false);
const [dialogView, setDialogView] = useState<DialogView | null>(null);
const [confirmText, setConfirmText] = useState('');
const [copied, setCopied] = useState(false);
const [, copyToClipboard] = useCopyToClipboard();
const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const { user, org } = useAppContext();
useEffect(
() => (): void => {
if (copyTimerRef.current) {
clearTimeout(copyTimerRef.current);
}
},
[],
);
const orgName = org?.[0]?.displayName ?? '';
const userEmail = user?.email ?? '';
const handleOpenCancelDialog = (): void => {
void logEvent('Billing : Cancel Subscription Clicked', {
user: pick(user, ['email', 'displayName', 'role', 'organization']),
role: user?.role,
});
setOpen(true);
setDialogView('confirm');
};
const handleContactSupport = (): void => {
@@ -29,43 +102,41 @@ function CancelSubscriptionBanner(): JSX.Element {
user: pick(user, ['email', 'displayName', 'role', 'organization']),
role: user?.role,
});
const subject = encodeURIComponent('Cancel My SigNoz Subscription');
const orgName = org?.[0]?.displayName ?? '';
const body = encodeURIComponent(
[
'Hi SigNoz Team,',
'',
'I would like to cancel my SigNoz Cloud subscription.',
'Please find my account details below.',
'',
'Account Details:',
` • SigNoz URL: ${getBaseUrl()}`,
...(orgName ? [` • Organization: ${orgName}`] : []),
` • Account Email: ${user?.email ?? ''}`,
'',
'Reason for Cancellation:',
'[Please share the reason for cancellation]',
'',
'Additional feedback (optional):',
'[Any other feedback]',
'',
'Regards,',
'[user name or team name]',
].join('\n'),
);
const link = document.createElement('a');
link.href = `mailto:cloud-support@signoz.io?subject=${subject}&body=${body}`;
link.click();
setOpen(false);
openMailto(buildMailtoUri(orgName, userEmail));
setConfirmText('');
setDialogView('fallback');
};
const handleCopyTemplate = (): void => {
void logEvent('Billing : Cancel Subscription Email Template Copied', {
user: pick(user, ['email', 'displayName', 'role', 'organization']),
role: user?.role,
});
copyToClipboard(buildEmailBody(orgName, userEmail));
setCopied(true);
if (copyTimerRef.current) {
clearTimeout(copyTimerRef.current);
}
copyTimerRef.current = setTimeout(() => setCopied(false), 2000);
};
const handleRetryMailto = (): void => {
void logEvent('Billing : Cancel Subscription Email Client Reopened', {
user: pick(user, ['email', 'displayName', 'role', 'organization']),
role: user?.role,
});
};
const handleClose = (): void => {
setOpen(false);
if (copyTimerRef.current) {
clearTimeout(copyTimerRef.current);
}
setDialogView(null);
setConfirmText('');
setCopied(false);
};
const footer = (
const confirmFooter = (
<>
<Button
variant="solid"
@@ -81,12 +152,19 @@ function CancelSubscriptionBanner(): JSX.Element {
prefix={<X size={14} />}
disabled={confirmText !== 'cancel'}
onClick={handleContactSupport}
data-testid="cancel-subscription-confirm-btn"
>
Cancel subscription
</Button>
</>
);
const fallbackFooter = (
<Button variant="solid" color="secondary" onClick={handleClose}>
Close
</Button>
);
return (
<>
<div className={styles.banner}>
@@ -111,27 +189,67 @@ function CancelSubscriptionBanner(): JSX.Element {
</Button>
</div>
<DialogWrapper
open={open}
open={dialogView !== null}
onOpenChange={handleClose}
title="Cancel your subscription?"
width="narrow"
showCloseButton={false}
footer={footer}
footer={dialogView === 'confirm' ? confirmFooter : fallbackFooter}
>
<div className={styles.dialogBody}>
<p className={styles.dialogDescription}>
Cancelling your subscription would stop your data from being ingested to
SigNoz. All the data that has been already sent will also be deleted.
</p>
<p className={styles.dialogConfirmLabel}>
Type <code>cancel</code> to confirm the cancellation.
</p>
<Input
placeholder="Enter the word cancel..."
value={confirmText}
onChange={(e): void => setConfirmText(e.target.value)}
/>
</div>
{dialogView === 'confirm' && (
<div className={styles.dialogBody}>
<p className={styles.dialogDescription}>
Cancelling your subscription would stop your data from being ingested to
SigNoz. All the data that has been already sent will also be deleted.
</p>
<p className={styles.dialogConfirmLabel}>
Type <code>cancel</code> to confirm the cancellation.
</p>
<Input
placeholder="Enter the word cancel..."
value={confirmText}
onChange={(e): void => setConfirmText(e.target.value)}
data-testid="cancel-confirm-input"
/>
</div>
)}
{dialogView === 'fallback' && (
<div className={styles.fallbackBody}>
<p className={styles.fallbackHint}>
An email draft has been opened. If it did not open, send your
cancellation request directly to:
</p>
<span className={styles.fallbackEmail}>{SUPPORT_EMAIL}</span>
<div className={styles.fallbackActions}>
<Button
variant="outlined"
color="secondary"
prefix={copied ? <CircleCheck size={14} /> : <Copy size={14} />}
onClick={handleCopyTemplate}
data-testid="copy-email-template-btn"
>
{copied ? 'Copied!' : 'Copy email template'}
</Button>
<Button
asChild
variant="outlined"
color="secondary"
data-testid="retry-mailto-btn"
>
<a
href={buildMailtoUri(orgName, userEmail)}
onClick={handleRetryMailto}
className={styles.retryLink}
target="_blank"
rel="noopener noreferrer"
>
<MailOpen size={14} />
Reopen email client
</a>
</Button>
</div>
</div>
)}
</DialogWrapper>
</>
);

View File

@@ -0,0 +1,46 @@
import { useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import logEvent from 'api/common/logEvent';
import Noz from 'components/Noz/Noz';
import { NOZ_TOOLTIP_TITLE } from 'components/Noz/Noz.constants';
import {
AIAssistantEvents,
AIAssistantOpenSource,
} from 'container/AIAssistant/events';
import { normalizePage } from 'container/AIAssistant/hooks/useAIAssistantAnalyticsContext';
import { openAIAssistant } from 'container/AIAssistant/store/useAIAssistantStore';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
export default function NozButton(): JSX.Element | null {
const { pathname } = useLocation();
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const handleOpenNoz = useCallback((): void => {
void logEvent(AIAssistantEvents.Opened, {
source: AIAssistantOpenSource.TraceDetails,
currentPage: normalizePage(pathname),
});
openAIAssistant();
}, [pathname]);
if (!isAIAssistantEnabled) {
return null;
}
return (
<TooltipSimple title={NOZ_TOOLTIP_TITLE}>
<Button
variant="ghost"
size="icon"
color="secondary"
className="noz-wave"
aria-label="Open Noz"
onClick={handleOpenNoz}
>
<Noz size={16} />
</Button>
</TooltipSimple>
);
}

View File

@@ -20,6 +20,7 @@ import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { uniqBy } from 'lodash-es';
import NozButton from 'pages/TraceDetailsV3/TraceDetailsHeader/NozButton';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import {
@@ -426,6 +427,8 @@ function Filters({
)}
</div>
<NozButton />
<div className={styles.highlightControl}>{highlightErrorsToggle}</div>
</div>
</TooltipProvider>

View File

@@ -95,7 +95,7 @@ export default defineConfig(({ mode }): UserConfig => {
project: env.VITE_SENTRY_PROJECT_ID,
// Pin the sourcemap-upload release to the same value injected as
// process.env.VERSION so uploaded sourcemaps resolve. Ref: platform-pod#2393
release: { name: env.VITE_VERSION },
release: { name: env.VITE_VERSION, setCommits: { auto: true } },
}),
);
}

View File

@@ -0,0 +1,34 @@
package signozapiserver
import (
"net/http"
"github.com/gorilla/mux"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/emptystatetypes"
)
func (provider *provider) addEmptyStateRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/empty_state/org_context", handler.New(
provider.authzMiddleware.ViewAccess(provider.emptyStateHandler.GetOrgContext),
handler.OpenAPIDef{
ID: "GetOrgContext",
Tags: []string{"emptystate"},
Summary: "Get org context for empty states",
Description: "This endpoint returns raw org-level observability signals used to render contextual empty states",
Request: nil,
RequestContentType: "",
Response: new(emptystatetypes.OrgContext),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
},
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/emptystate"
"github.com/SigNoz/signoz/pkg/modules/fields"
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
@@ -57,6 +58,7 @@ type provider struct {
infraMonitoringHandler inframonitoring.Handler
gatewayHandler gateway.Handler
fieldsHandler fields.Handler
emptyStateHandler emptystate.Handler
authzHandler authz.Handler
rawDataExportHandler rawdataexport.Handler
zeusHandler zeus.Handler
@@ -89,6 +91,7 @@ func NewFactory(
infraMonitoringHandler inframonitoring.Handler,
gatewayHandler gateway.Handler,
fieldsHandler fields.Handler,
emptyStateHandler emptystate.Handler,
authzHandler authz.Handler,
rawDataExportHandler rawdataexport.Handler,
zeusHandler zeus.Handler,
@@ -124,6 +127,7 @@ func NewFactory(
infraMonitoringHandler,
gatewayHandler,
fieldsHandler,
emptyStateHandler,
authzHandler,
rawDataExportHandler,
zeusHandler,
@@ -161,6 +165,7 @@ func newProvider(
infraMonitoringHandler inframonitoring.Handler,
gatewayHandler gateway.Handler,
fieldsHandler fields.Handler,
emptyStateHandler emptystate.Handler,
authzHandler authz.Handler,
rawDataExportHandler rawdataexport.Handler,
zeusHandler zeus.Handler,
@@ -197,6 +202,7 @@ func newProvider(
infraMonitoringHandler: infraMonitoringHandler,
gatewayHandler: gatewayHandler,
fieldsHandler: fieldsHandler,
emptyStateHandler: emptyStateHandler,
authzHandler: authzHandler,
rawDataExportHandler: rawDataExportHandler,
zeusHandler: zeusHandler,
@@ -286,6 +292,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addEmptyStateRoutes(router); err != nil {
return err
}
if err := provider.addRawDataExportRoutes(router); err != nil {
return err
}

View File

@@ -22,7 +22,7 @@ func newConfig() factory.Config {
Agent: AgentConfig{
// we will maintain the latest version of cloud integration agent from here,
// till we automate it externally or figure out a way to validate it.
Version: "v0.0.10",
Version: "v0.0.11",
},
}
}

View File

@@ -0,0 +1 @@
<svg id="b70acf0a-34b4-4bdf-9024-7496043ff915" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><defs><radialGradient id="e2cf8746-c9a8-4eee-86c2-4951983c6032" cx="13428.81" cy="3518.86" r="56.67" gradientTransform="translate(-2005.33 -518.83) scale(0.15)" gradientUnits="userSpaceOnUse"><stop offset="0.18" stop-color="#5ea0ef"/><stop offset="1" stop-color="#0078d4"/></radialGradient><linearGradient id="bdd213dd-d313-473c-8ff4-0133fd3a9033" x1="4.4" y1="11.48" x2="4.37" y2="7.53" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ccc"/><stop offset="1" stop-color="#fcfcfc"/></linearGradient><linearGradient id="afcc63c5-3649-4476-a742-bcb53a569f3c" x1="10.13" y1="15.45" x2="10.13" y2="11.9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ccc"/><stop offset="1" stop-color="#fcfcfc"/></linearGradient><linearGradient id="bd873f0b-9954-4aa5-a3df-9f4c64e8729d" x1="14.18" y1="11.15" x2="14.18" y2="7.38" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ccc"/><stop offset="1" stop-color="#fcfcfc"/></linearGradient></defs><title>Icon-web-41</title><path id="ee75dd06-1aca-4f76-9d11-d05a284020ad" d="M14.21,15.72A8.5,8.5,0,0,1,3.79,2.28l.09-.06a8.5,8.5,0,0,1,10.33,13.5" fill="url(#e2cf8746-c9a8-4eee-86c2-4951983c6032)"/><path d="M6.69,7.23A13,13,0,0,1,15.6,3.65a8.47,8.47,0,0,0-1.49-1.44,14.34,14.34,0,0,0-4.69,1.1A12.54,12.54,0,0,0,5.34,6.13,2.76,2.76,0,0,1,6.69,7.23Z" fill="#fff" opacity="0.6"/><path d="M2.48,10.65a17.86,17.86,0,0,0-.83,2.62,7.82,7.82,0,0,0,.62.92c.18.23.35.44.55.65A17.94,17.94,0,0,1,3.9,11.37,2.76,2.76,0,0,1,2.48,10.65Z" fill="#fff" opacity="0.6"/><path d="M3.46,6.11a12,12,0,0,1-.69-2.94,8.15,8.15,0,0,0-1.1,1.45A12.69,12.69,0,0,0,2.24,7,2.69,2.69,0,0,1,3.46,6.11Z" fill="#f2f2f2" opacity="0.55"/><circle cx="4.38" cy="8.68" r="2.73" fill="url(#bdd213dd-d313-473c-8ff4-0133fd3a9033)"/><path d="M8.36,13.67A1.77,1.77,0,0,1,8.9,12.4a11.88,11.88,0,0,1-2.53-1.86,2.74,2.74,0,0,1-1.49.83,13.1,13.1,0,0,0,1.45,1.28A12.12,12.12,0,0,0,8.38,13.9,1.79,1.79,0,0,1,8.36,13.67Z" fill="#f2f2f2" opacity="0.55"/><path d="M14.66,13.88a12,12,0,0,1-2.76-.32.41.41,0,0,1,0,.11,1.75,1.75,0,0,1-.51,1.24,13.69,13.69,0,0,0,3.42.24A8.21,8.21,0,0,0,16,13.81,11.5,11.5,0,0,1,14.66,13.88Z" fill="#f2f2f2" opacity="0.55"/><circle cx="10.13" cy="13.67" r="1.78" fill="url(#afcc63c5-3649-4476-a742-bcb53a569f3c)"/><path d="M12.32,8.93a1.83,1.83,0,0,1,.61-1A25.5,25.5,0,0,1,8.47,3.79a16.91,16.91,0,0,1-2-2.92,7.64,7.64,0,0,0-1.09.42A18.14,18.14,0,0,0,7.53,4.47,26.44,26.44,0,0,0,12.32,8.93Z" fill="#f2f2f2" opacity="0.7"/><circle cx="14.18" cy="9.27" r="1.89" fill="url(#bd873f0b-9954-4aa5-a3df-9f4c64e8729d)"/><path d="M17.35,10.54,17,10.37l0,0-.3-.16-.06,0L16.38,10l-.07,0L16,9.8a1.76,1.76,0,0,1-.64.92c.12.08.25.15.38.22l.08.05.35.19,0,0,.86.45h0a8.63,8.63,0,0,0,.29-1.11Z" fill="#f2f2f2" opacity="0.55"/><circle cx="4.38" cy="8.68" r="2.73" fill="url(#bdd213dd-d313-473c-8ff4-0133fd3a9033)"/><circle cx="10.13" cy="13.67" r="1.78" fill="url(#afcc63c5-3649-4476-a742-bcb53a569f3c)"/></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,272 @@
{
"id": "appservice",
"title": "App Services",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"supportedSignals": {
"metrics": true,
"logs": true
},
"dataCollected": {
"metrics": [
{
"name": "azure_averagememoryworkingset_average",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_bytesreceived_total",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_bytessent_total",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_backendrequestcount_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_cputime_count",
"unit": "Milliseconds",
"type": "Gauge",
"description": ""
},
{
"name": "azure_cputime_total",
"unit": "Milliseconds",
"type": "Gauge",
"description": ""
},
{
"name": "azure_cputime_minimum",
"unit": "Milliseconds",
"type": "Gauge",
"description": ""
},
{
"name": "azure_cputime_maximum",
"unit": "Milliseconds",
"type": "Gauge",
"description": ""
},
{
"name": "azure_currentassemblies_average",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_filesystemusage_average",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_gen0collections_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_ge10collections_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_gen2collections_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_handles_average",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_healthcheckstatus_average",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_http101_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_http2xx_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_http3xx_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_http401_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_http403_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_http404_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_http406_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_http4xx_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_http5xx_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_httpresponsetime_average",
"unit": "Milliseconds",
"type": "Gauge",
"description": ""
},
{
"name": "azure_iootherbytespersecond_total",
"unit": "BytesPerSecond",
"type": "Gauge",
"description": ""
},
{
"name": "azure_iootheroperationspersecond_total",
"unit": "BytesPerSecond",
"type": "Gauge",
"description": ""
},
{
"name": "azure_ioreadbytespersecond_total",
"unit": "BytesPerSecond",
"type": "Gauge",
"description": ""
},
{
"name": "azure_ioreadoperationspersecond_total",
"unit": "BytesPerSecond",
"type": "Gauge",
"description": ""
},
{
"name": "azure_iowritebytespersecond_total",
"unit": "BytesPerSecond",
"type": "Gauge",
"description": ""
},
{
"name": "azure_iowriteoperationspersecond_total",
"unit": "BytesPerSecond",
"type": "Gauge",
"description": ""
},
{
"name": "azure_privatebytes_average",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_requests_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_requestsinapplicationqueue_average",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_thread_average",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_totalappdomains_average",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_totalappdomainsunloaded_average",
"unit": "Count",
"type": "Gauge",
"description": ""
}
],
"logs": [
{
"name": "Resource ID",
"path": "resources.azure.resource.id",
"type": "string"
}
]
},
"telemetryCollectionStrategy": {
"azure": {
"resourceProvider": "Microsoft.Web",
"resourceType": "sites",
"metrics": {},
"logs": {
"categoryGroups": ["allLogs"]
}
}
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "App Services Overview",
"description": "Overview of App Services metrics",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

@@ -0,0 +1,5 @@
### Monitor Azure App Services with SigNoz
Collect key App Services metrics and view them with an out of the box dashboard.
Note: This integration DO NOT collect metrics for any database that was setup with your App Service (if any).

View File

@@ -0,0 +1 @@
<svg id="fd454f1c-5506-44b8-874e-8814b8b2f70b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><defs><linearGradient id="f34d9569-2bd0-4002-8f16-3d01d8106cb5" x1="8.88" y1="12.21" x2="8.88" y2="0.21" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0078d4"/><stop offset="0.82" stop-color="#5ea0ef"/></linearGradient><linearGradient id="bdb45a0b-eb58-4970-a60a-fb2ce314f866" x1="8.88" y1="16.84" x2="8.88" y2="12.21" gradientUnits="userSpaceOnUse"><stop offset="0.15" stop-color="#ccc"/><stop offset="1" stop-color="#707070"/></linearGradient></defs><title>Icon-compute-21</title><rect x="-0.12" y="0.21" width="18" height="12" rx="0.6" fill="url(#f34d9569-2bd0-4002-8f16-3d01d8106cb5)"/><polygon points="11.88 4.46 11.88 7.95 8.88 9.71 8.88 6.21 11.88 4.46" fill="#50e6ff"/><polygon points="11.88 4.46 8.88 6.22 5.88 4.46 8.88 2.71 11.88 4.46" fill="#c3f1ff"/><polygon points="8.88 6.22 8.88 9.71 5.88 7.95 5.88 4.46 8.88 6.22" fill="#9cebff"/><polygon points="5.88 7.95 8.88 6.21 8.88 9.71 5.88 7.95" fill="#c3f1ff"/><polygon points="11.88 7.95 8.88 6.21 8.88 9.71 11.88 7.95" fill="#9cebff"/><path d="M12.49,15.84c-1.78-.28-1.85-1.56-1.85-3.63H7.11c0,2.07-.06,3.35-1.84,3.63a1,1,0,0,0-.89,1h9A1,1,0,0,0,12.49,15.84Z" fill="url(#bdb45a0b-eb58-4970-a60a-fb2ce314f866)"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
### Monitor Azure Virtual Machines with SigNoz
Collect key Virtual Machines metrics and view them with an out of the box dashboard.

View File

@@ -0,0 +1,17 @@
package emptystate
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types/emptystatetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
GetOrgContext(ctx context.Context, orgID valuer.UUID) (*emptystatetypes.OrgContext, error)
}
type Handler interface {
GetOrgContext(rw http.ResponseWriter, req *http.Request)
}

View File

@@ -0,0 +1,116 @@
package implemptystate
import (
"context"
"fmt"
"time"
"github.com/huandu/go-sqlbuilder"
amalert "github.com/prometheus/alertmanager/api/v2/restapi/operations/alert"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/emptystatetypes"
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var ruleStateHistoryTableFQN = fmt.Sprintf("%s.%s", rulestatehistorytypes.DBName, rulestatehistorytypes.TableName)
func (m *module) getRuleIDs(ctx context.Context, orgID valuer.UUID) ([]string, error) {
ruleIDs := make([]string, 0)
if err := m.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model((*ruletypes.StorableRule)(nil)).
Column("id").
Where("org_id = ?", orgID.StringValue()).
Where("deleted = ?", 0).
Scan(ctx, &ruleIDs); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to list org rule ids")
}
return ruleIDs, nil
}
func (m *module) getActiveFiringAlertsCount(ctx context.Context, orgID valuer.UUID) (int, error) {
if m.alertmanager == nil {
return 0, errors.NewInternalf(errors.CodeInternal, "alertmanager is not configured")
}
active := true
silenced := false
inhibited := false
unprocessed := true
params := alertmanagertypes.GettableAlertsParams{
GetAlertsParams: amalert.NewGetAlertsParams(),
}
params.Active = &active
params.Silenced = &silenced
params.Inhibited = &inhibited
params.Unprocessed = &unprocessed
alerts, err := m.alertmanager.GetAlerts(ctx, orgID.StringValue(), params)
if err != nil {
return 0, errors.WrapInternalf(err, errors.CodeInternal, "failed to get active alerts from alertmanager")
}
return len(alerts), nil
}
// The history table has no org_id: fetch distinct fired rule IDs in the window
// and intersect with org rules in Go (an IN clause can exceed max_query_size).
func (m *module) getRecentlyFiredAlertsCount(ctx context.Context, ruleIDs []string, now time.Time) (int, error) {
if len(ruleIDs) == 0 {
return 0, nil
}
query, args := buildRecentlyFiredRuleIDsQuery(now)
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return 0, errors.WrapInternalf(err, errors.CodeInternal, "failed to query recently fired rule ids")
}
defer rows.Close()
orgRuleIDs := make(map[string]struct{}, len(ruleIDs))
for _, ruleID := range ruleIDs {
orgRuleIDs[ruleID] = struct{}{}
}
count := 0
for rows.Next() {
var ruleID string
if err := rows.Scan(&ruleID); err != nil {
return 0, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan recently fired rule id")
}
if _, ok := orgRuleIDs[ruleID]; ok {
count++
}
}
if err := rows.Err(); err != nil {
return 0, errors.WrapInternalf(err, errors.CodeInternal, "failed to read recently fired rule ids")
}
return count, nil
}
func buildRecentlyFiredRuleIDsQuery(now time.Time) (string, []any) {
cutoff := now.Add(-emptystatetypes.RecentlyFiredAlertsWindow)
sb := sqlbuilder.NewSelectBuilder()
sb.Select("DISTINCT rule_id")
sb.From(ruleStateHistoryTableFQN)
sb.Where(
sb.E("state", ruletypes.StateFiring.StringValue()),
sb.E("state_changed", true),
sb.GE("unix_milli", cutoff.UnixMilli()),
sb.LE("unix_milli", now.UnixMilli()),
)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return query, args
}

View File

@@ -0,0 +1,34 @@
package implemptystate
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/emptystate"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type handler struct {
module emptystate.Module
}
func NewHandler(module emptystate.Module) emptystate.Handler {
return &handler{module: module}
}
func (handler *handler) GetOrgContext(rw http.ResponseWriter, req *http.Request) {
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil {
render.Error(rw, err)
return
}
orgContext, err := handler.module.GetOrgContext(req.Context(), valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, orgContext)
}

View File

@@ -0,0 +1,147 @@
package implemptystate
import (
"context"
"database/sql"
"fmt"
"slices"
"strings"
"time"
"github.com/huandu/go-sqlbuilder"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
"github.com/SigNoz/signoz/pkg/telemetrytraces"
"github.com/SigNoz/signoz/pkg/types/emptystatetypes"
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
)
var infraMetricNames = withUnderscoreMetricNames(slices.Concat(
inframonitoringtypes.HostsTableMetricNames,
inframonitoringtypes.PodsTableMetricNames,
inframonitoringtypes.NodesTableMetricNames,
))
// Span-derived metrics (signoz_calls_total, signoz_latency, ...) come from the
// collector's spanmetrics processor, so they must not count as metrics ingestion.
const spanGeneratedMetricsLikePattern = `signoz\_%`
// Probe both dot-form and underscore-normalized names (dot_metrics_enabled).
func withUnderscoreMetricNames(metricNames []string) []string {
seen := make(map[string]struct{}, len(metricNames)*2)
normalized := make([]string, 0, len(metricNames)*2)
for _, metricName := range metricNames {
for _, candidate := range []string{metricName, strings.ReplaceAll(metricName, ".", "_")} {
if _, ok := seen[candidate]; ok {
continue
}
seen[candidate] = struct{}{}
normalized = append(normalized, candidate)
}
}
return normalized
}
func (m *module) getWindowedPresence(ctx context.Context, exists func(context.Context, time.Time, time.Time) (bool, error), now time.Time) (signalPresence, error) {
oneHour, err := exists(ctx, now.Add(-emptystatetypes.IngestingCurrentlyWindow), now)
if err != nil {
return signalPresence{}, err
}
if oneHour {
return signalPresence{oneHour: true, sevenDay: true}, nil
}
sevenDay, err := exists(ctx, now.Add(-emptystatetypes.HasIngestedDataWindow), now)
if err != nil {
return signalPresence{}, err
}
return signalPresence{oneHour: false, sevenDay: sevenDay}, nil
}
// Probes are deployment-scoped (telemetry tables have no org_id) and bounded
// on both sides so future-dated rows cannot pin a signal true.
func (m *module) logsExist(ctx context.Context, cutoff time.Time, now time.Time) (bool, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select("1")
sb.From(fmt.Sprintf("%s.%s", telemetrylogs.DBName, telemetrylogs.LogsV2TableName))
sb.Where(
sb.GE("ts_bucket_start", cutoff.Unix()-querybuilder.BucketAdjustment),
sb.LE("ts_bucket_start", now.Unix()),
sb.GE("timestamp", fmt.Sprintf("%d", cutoff.UnixNano())),
sb.LE("timestamp", fmt.Sprintf("%d", now.UnixNano())),
)
sb.Limit(1)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return m.exists(ctx, "logs presence", query, args...)
}
func (m *module) tracesExist(ctx context.Context, cutoff time.Time, now time.Time) (bool, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select("1")
sb.From(fmt.Sprintf("%s.%s", telemetrytraces.DBName, telemetrytraces.SpanIndexV3TableName))
sb.Where(
sb.GE("ts_bucket_start", cutoff.Unix()-querybuilder.BucketAdjustment),
sb.LE("ts_bucket_start", now.Unix()),
sb.GE("timestamp", fmt.Sprintf("%d", cutoff.UnixNano())),
sb.LE("timestamp", fmt.Sprintf("%d", now.UnixNano())),
)
sb.Limit(1)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return m.exists(ctx, "traces presence", query, args...)
}
func (m *module) metricsExist(ctx context.Context, cutoff time.Time, now time.Time) (bool, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select("1")
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.AttributesMetadataTableName))
sb.Where(
sb.GE("last_reported_unix_milli", cutoff.UnixMilli()),
sb.LE("last_reported_unix_milli", now.UnixMilli()),
sb.NotLike("metric_name", spanGeneratedMetricsLikePattern),
)
sb.Limit(1)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return m.exists(ctx, "metrics presence", query, args...)
}
func (m *module) getHasInfraMetrics(ctx context.Context, now time.Time) (bool, error) {
cutoff := now.Add(-emptystatetypes.HasIngestedDataWindow)
sb := sqlbuilder.NewSelectBuilder()
sb.Select("1")
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.AttributesMetadataTableName))
sb.Where(
sb.GE("last_reported_unix_milli", cutoff.UnixMilli()),
sb.LE("last_reported_unix_milli", now.UnixMilli()),
sb.In("metric_name", sqlbuilder.List(infraMetricNames)),
)
sb.Limit(1)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return m.exists(ctx, "infra metrics presence", query, args...)
}
func (m *module) exists(ctx context.Context, probe string, query string, args ...any) (bool, error) {
var exists uint8
err := m.telemetryStore.ClickhouseDB().QueryRow(ctx, query, args...).Scan(&exists)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return false, errors.WrapInternalf(err, errors.CodeInternal, "failed to check %s", probe)
}
return true, nil
}

View File

@@ -0,0 +1,204 @@
package implemptystate
import (
"context"
"strings"
"time"
"golang.org/x/sync/errgroup"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/emptystate"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/emptystatetypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct {
telemetryStore telemetrystore.TelemetryStore
sqlstore sqlstore.SQLStore
alertmanager alertmanager.Alertmanager
licensing licensing.Licensing
}
type signalPresence struct {
sevenDay bool
oneHour bool
}
func NewModule(
telemetryStore telemetrystore.TelemetryStore,
sqlstore sqlstore.SQLStore,
alertmanager alertmanager.Alertmanager,
licensing licensing.Licensing,
) emptystate.Module {
return &module{
telemetryStore: telemetryStore,
sqlstore: sqlstore,
alertmanager: alertmanager,
licensing: licensing,
}
}
func (m *module) GetOrgContext(ctx context.Context, orgID valuer.UUID) (*emptystatetypes.OrgContext, error) {
now := time.Now()
var logsPresence signalPresence
var tracesPresence signalPresence
var metricsPresence signalPresence
var hasInfraMetrics bool
var alertsCount int
var activeFiringAlertsCount int
var recentlyFiredAlertsCount int
var dashboardsCount int
var savedViewsCount int
licenseStatus := emptystatetypes.LicenseStatusUnknown
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
presence, err := m.getWindowedPresence(gCtx, m.logsExist, now)
if err != nil {
return err
}
logsPresence = presence
return nil
})
g.Go(func() error {
presence, err := m.getWindowedPresence(gCtx, m.tracesExist, now)
if err != nil {
return err
}
tracesPresence = presence
return nil
})
g.Go(func() error {
presence, err := m.getWindowedPresence(gCtx, m.metricsExist, now)
if err != nil {
return err
}
metricsPresence = presence
return nil
})
g.Go(func() error {
var err error
hasInfraMetrics, err = m.getHasInfraMetrics(gCtx, now)
if err != nil {
return err
}
return nil
})
g.Go(func() error {
ruleIDs, err := m.getRuleIDs(gCtx, orgID)
if err != nil {
return err
}
alertsCount = len(ruleIDs)
recentlyFired, err := m.getRecentlyFiredAlertsCount(gCtx, ruleIDs, now)
if err != nil {
return err
}
recentlyFiredAlertsCount = recentlyFired
return nil
})
g.Go(func() error {
var err error
activeFiringAlertsCount, err = m.getActiveFiringAlertsCount(gCtx, orgID)
if err != nil {
return err
}
return nil
})
g.Go(func() error {
var err error
dashboardsCount, err = m.getDashboardsCount(gCtx, orgID)
if err != nil {
return err
}
return nil
})
g.Go(func() error {
var err error
savedViewsCount, err = m.getSavedViewsCount(gCtx, orgID)
if err != nil {
return err
}
return nil
})
g.Go(func() error {
licenseStatus = m.getLicenseStatus(gCtx, orgID)
return nil
})
if err := g.Wait(); err != nil {
return nil, err
}
signalsIngested := emptystatetypes.SignalsIngested{
Logs: logsPresence.sevenDay,
Traces: tracesPresence.sevenDay,
Metrics: metricsPresence.sevenDay,
}
return &emptystatetypes.OrgContext{
HasIngestedData: signalsIngested.Logs || signalsIngested.Traces || signalsIngested.Metrics,
SignalsIngested: signalsIngested,
IngestingCurrently: logsPresence.oneHour || tracesPresence.oneHour || metricsPresence.oneHour,
HasInfraMetrics: hasInfraMetrics,
AlertsCount: alertsCount,
ActiveFiringAlertsCount: activeFiringAlertsCount,
RecentlyFiredAlertsCount: recentlyFiredAlertsCount,
DashboardsCount: dashboardsCount,
SavedViewsCount: savedViewsCount,
LicenseStatus: licenseStatus,
}, nil
}
// Community wires nooplicensing whose GetActive always errors, so license
// failures degrade to UNKNOWN instead of failing the endpoint.
func (m *module) getLicenseStatus(ctx context.Context, orgID valuer.UUID) emptystatetypes.LicenseStatus {
if m.licensing == nil {
return emptystatetypes.LicenseStatusUnknown
}
license, err := m.licensing.GetActive(ctx, orgID)
if err != nil {
return emptystatetypes.LicenseStatusUnknown
}
return NewLicenseStatusFromLicense(license)
}
func NewLicenseStatusFromLicense(license *licensetypes.License) emptystatetypes.LicenseStatus {
if license == nil {
return emptystatetypes.LicenseStatusUnknown
}
if strings.TrimSpace(license.State) == "" {
return emptystatetypes.LicenseStatusUnknown
}
// Verbatim passthrough: trimming above is only for blank detection.
return emptystatetypes.LicenseStatus(license.State)
}

View File

@@ -0,0 +1,367 @@
package implemptystate
import (
"context"
"regexp"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
cmock "github.com/SigNoz/clickhouse-go-mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertest"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/emptystatetypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var testOrgID = valuer.MustNewUUID("00000000-0000-0000-0000-000000000001")
var (
ruleIDsQueryRegex = `SELECT "storable_rule"\."id" FROM "rule" AS "storable_rule" WHERE \(org_id = '` + testOrgID.StringValue() + `'\) AND \(deleted = 0\)`
dashboardsCountQueryRegex = `SELECT count\(\*\) FROM "dashboard" WHERE \(org_id = '` + testOrgID.StringValue() + `'\) AND \(source = '` + dashboardtypes.SourceUser.StringValue() + `'\)`
)
type fakeLicensing struct {
licensing.Licensing
license *licensetypes.License
err error
}
func (f *fakeLicensing) GetActive(context.Context, valuer.UUID) (*licensetypes.License, error) {
return f.license, f.err
}
func TestNewLicenseStatusFromLicense(t *testing.T) {
knownStates := []string{
"DEFAULTED",
"ACTIVATED",
"EXPIRED",
"ISSUED",
"EVALUATING",
"EVALUATION_EXPIRED",
"TERMINATED",
"CANCELLED",
}
for _, state := range knownStates {
t.Run(state, func(t *testing.T) {
status := NewLicenseStatusFromLicense(&licensetypes.License{State: state})
assert.Equal(t, emptystatetypes.LicenseStatus(state), status)
})
}
t.Run("novel state passes through", func(t *testing.T) {
status := NewLicenseStatusFromLicense(&licensetypes.License{State: "FUTURE_STATE"})
assert.Equal(t, emptystatetypes.LicenseStatus("FUTURE_STATE"), status)
})
t.Run("nil license returns unknown", func(t *testing.T) {
status := NewLicenseStatusFromLicense(nil)
assert.Equal(t, emptystatetypes.LicenseStatusUnknown, status)
})
t.Run("empty state returns unknown", func(t *testing.T) {
status := NewLicenseStatusFromLicense(&licensetypes.License{State: " "})
assert.Equal(t, emptystatetypes.LicenseStatusUnknown, status)
})
}
func TestGetLicenseStatusErrorDegradesToUnknown(t *testing.T) {
mod := &module{licensing: &fakeLicensing{err: assert.AnError}}
status := mod.getLicenseStatus(context.Background(), testOrgID)
assert.Equal(t, emptystatetypes.LicenseStatusUnknown, status)
}
func TestGetWindowedPresenceShortCircuit(t *testing.T) {
mod := &module{}
now := time.Unix(1000, 0)
t.Run("one hour implies seven day", func(t *testing.T) {
calls := 0
presence, err := mod.getWindowedPresence(context.Background(), func(context.Context, time.Time, time.Time) (bool, error) {
calls++
return true, nil
}, now)
require.NoError(t, err)
assert.Equal(t, signalPresence{oneHour: true, sevenDay: true}, presence)
assert.Equal(t, 1, calls)
})
t.Run("falls back to seven day when one hour is empty", func(t *testing.T) {
calls := 0
presence, err := mod.getWindowedPresence(context.Background(), func(context.Context, time.Time, time.Time) (bool, error) {
calls++
return calls == 2, nil
}, now)
require.NoError(t, err)
assert.Equal(t, signalPresence{oneHour: false, sevenDay: true}, presence)
assert.Equal(t, 2, calls)
})
}
func TestRecentlyFiredAlertsCountNoRulesDoesNotQueryClickHouse(t *testing.T) {
ts := telemetrystoretest.New(telemetrystore.Config{}, sqlmock.QueryMatcherRegexp)
mod := &module{telemetryStore: ts}
count, err := mod.getRecentlyFiredAlertsCount(context.Background(), nil, time.Now())
require.NoError(t, err)
assert.Equal(t, 0, count)
assert.NoError(t, ts.Mock().ExpectationsWereMet())
}
func TestRuleIDScopingExcludesDeletedRules(t *testing.T) {
ss := sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherRegexp)
mod := &module{sqlstore: ss}
ss.Mock().
ExpectQuery(ruleIDsQueryRegex).
WillReturnRows(ss.Mock().NewRows([]string{"id"}).AddRow("active-rule").AddRow("another-active-rule"))
ruleIDs, err := mod.getRuleIDs(context.Background(), testOrgID)
require.NoError(t, err)
assert.Equal(t, []string{"active-rule", "another-active-rule"}, ruleIDs)
assert.NoError(t, ss.Mock().ExpectationsWereMet())
}
func TestRecentlyFiredRuleIDsQueryIsWindowBounded(t *testing.T) {
query, args := buildRecentlyFiredRuleIDsQuery(time.Unix(1000, 0))
assert.Contains(t, query, "DISTINCT rule_id")
assert.Contains(t, query, "state = ?")
assert.Contains(t, query, "state_changed = ?")
assert.Contains(t, query, "unix_milli >= ?")
assert.Contains(t, query, "unix_milli <= ?")
assert.Len(t, args, 4)
}
func TestRecentlyFiredAlertsCountIntersectsOrgRules(t *testing.T) {
ts := telemetrystoretest.New(telemetrystore.Config{}, sqlmock.QueryMatcherRegexp)
mod := &module{telemetryStore: ts}
now := time.Unix(10000, 0)
cutoff := now.Add(-emptystatetypes.RecentlyFiredAlertsWindow)
// Window had two firing rules; only the one owned by the org counts.
ts.Mock().
ExpectQuery(`DISTINCT rule_id`).
WithArgs(ruletypes.StateFiring.StringValue(), true, cutoff.UnixMilli(), now.UnixMilli()).
WillReturnRows(cmock.NewRows(
[]cmock.ColumnType{{Name: "rule_id", Type: "String"}},
[][]any{{"org-rule"}, {"foreign-rule"}},
))
count, err := mod.getRecentlyFiredAlertsCount(context.Background(), []string{"org-rule", "other-org-rule"}, now)
require.NoError(t, err)
assert.Equal(t, 1, count)
assert.NoError(t, ts.Mock().ExpectationsWereMet())
}
func TestDashboardsCountFiltersToUserSource(t *testing.T) {
ss := sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherRegexp)
mod := &module{sqlstore: ss}
ss.Mock().
ExpectQuery(dashboardsCountQueryRegex).
WillReturnRows(ss.Mock().NewRows([]string{"count"}).AddRow(3))
count, err := mod.getDashboardsCount(context.Background(), testOrgID)
require.NoError(t, err)
assert.Equal(t, 3, count)
assert.NoError(t, ss.Mock().ExpectationsWereMet())
}
func TestActiveFiringAlertsParamsExcludeSilencedAndInhibited(t *testing.T) {
alertmanager := alertmanagertest.NewMockAlertmanager(t)
mod := &module{alertmanager: alertmanager}
alertmanager.EXPECT().
GetAlerts(mock.Anything, testOrgID.StringValue(), mock.MatchedBy(func(params alertmanagertypes.GettableAlertsParams) bool {
return params.Active != nil && *params.Active &&
params.Silenced != nil && !*params.Silenced &&
params.Inhibited != nil && !*params.Inhibited &&
params.Unprocessed != nil && *params.Unprocessed
})).
Return(alertmanagertypes.DeprecatedGettableAlerts{
&alertmanagertypes.DeprecatedGettableAlert{},
&alertmanagertypes.DeprecatedGettableAlert{},
}, nil)
count, err := mod.getActiveFiringAlertsCount(context.Background(), testOrgID)
require.NoError(t, err)
assert.Equal(t, 2, count)
}
func TestLogsExistQueryBoundsWindowOnBothSides(t *testing.T) {
ts := telemetrystoretest.New(telemetrystore.Config{}, sqlmock.QueryMatcherRegexp)
mod := &module{telemetryStore: ts}
now := time.Unix(2000, 0)
ts.Mock().
ExpectQueryRow(`ts_bucket_start >= \? AND ts_bucket_start <= \? AND timestamp >= \? AND timestamp <= \?`).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{{Name: "exists", Type: "UInt8"}}, []any{uint8(1)}))
exists, err := mod.logsExist(context.Background(), now.Add(-time.Hour), now)
require.NoError(t, err)
assert.True(t, exists)
assert.NoError(t, ts.Mock().ExpectationsWereMet())
}
func TestMetricsExistQueryExcludesSpanGeneratedMetrics(t *testing.T) {
ts := telemetrystoretest.New(telemetrystore.Config{}, sqlmock.QueryMatcherRegexp)
mod := &module{telemetryStore: ts}
now := time.Unix(2000, 0)
ts.Mock().
ExpectQueryRow(`last_reported_unix_milli >= \? AND last_reported_unix_milli <= \? AND metric_name NOT LIKE \?`).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{{Name: "exists", Type: "UInt8"}}, []any{uint8(1)}))
exists, err := mod.metricsExist(context.Background(), now.Add(-time.Hour), now)
require.NoError(t, err)
assert.True(t, exists)
assert.NoError(t, ts.Mock().ExpectationsWereMet())
}
func TestGetOrgContextDerivesAggregatesFromLogsOnly(t *testing.T) {
mod, chMock, sqlMock := newOrgContextTestModule(t, &fakeLicensing{license: &licensetypes.License{State: "ACTIVATED"}}, nil)
// Logs exist in the 1h window, so the 7d probe for logs is skipped.
expectCHExists(chMock, "signoz_logs.distributed_logs_v2", true)
expectCHExists(chMock, "signoz_traces.distributed_signoz_index_v3", false)
expectCHExists(chMock, "signoz_traces.distributed_signoz_index_v3", false)
expectCHExists(chMock, "signoz_metrics.distributed_metadata", false)
expectCHExists(chMock, "signoz_metrics.distributed_metadata", false)
expectCHExists(chMock, "signoz_metrics.distributed_metadata", false)
expectSQLCounts(sqlMock, 0, 0, nil)
orgContext, err := mod.GetOrgContext(context.Background(), testOrgID)
require.NoError(t, err)
assert.True(t, orgContext.HasIngestedData)
assert.True(t, orgContext.IngestingCurrently)
assert.Equal(t, emptystatetypes.SignalsIngested{Logs: true, Traces: false, Metrics: false}, orgContext.SignalsIngested)
assert.False(t, orgContext.HasInfraMetrics)
assert.Equal(t, emptystatetypes.LicenseStatus("ACTIVATED"), orgContext.LicenseStatus)
assert.NoError(t, chMock.ExpectationsWereMet())
assert.NoError(t, sqlMock.ExpectationsWereMet())
}
func TestGetOrgContextLicenseErrorDoesNotFail(t *testing.T) {
mod, chMock, sqlMock := newOrgContextTestModule(t, &fakeLicensing{err: assert.AnError}, nil)
expectTelemetryQuiet(chMock)
expectSQLCounts(sqlMock, 0, 0, nil)
orgContext, err := mod.GetOrgContext(context.Background(), testOrgID)
require.NoError(t, err)
assert.Equal(t, emptystatetypes.LicenseStatusUnknown, orgContext.LicenseStatus)
assert.NoError(t, chMock.ExpectationsWereMet())
assert.NoError(t, sqlMock.ExpectationsWereMet())
}
func TestGetOrgContextClickHouseErrorFails(t *testing.T) {
mod, chMock, sqlMock := newOrgContextTestModule(t, &fakeLicensing{license: &licensetypes.License{State: "ACTIVATED"}}, nil)
chMock.ExpectQueryRow(regexp.QuoteMeta("signoz_logs.distributed_logs_v2")).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{{Name: "exists", Type: "UInt8"}}, nil)).
WillReturnError(assert.AnError)
expectCHExists(chMock, "signoz_traces.distributed_signoz_index_v3", false)
expectCHExists(chMock, "signoz_traces.distributed_signoz_index_v3", false)
expectCHExists(chMock, "signoz_metrics.distributed_metadata", false)
expectCHExists(chMock, "signoz_metrics.distributed_metadata", false)
expectCHExists(chMock, "signoz_metrics.distributed_metadata", false)
expectSQLCounts(sqlMock, 0, 0, nil)
orgContext, err := mod.GetOrgContext(context.Background(), testOrgID)
assert.Error(t, err)
assert.Nil(t, orgContext)
}
func newOrgContextTestModule(t *testing.T, licensing licensing.Licensing, alertmanagerAlerts alertmanagertypes.DeprecatedGettableAlerts) (*module, cmock.ClickConnMockCommon, sqlmock.Sqlmock) {
t.Helper()
ts := telemetrystoretest.New(telemetrystore.Config{}, sqlmock.QueryMatcherRegexp)
ts.Mock().MatchExpectationsInOrder(false)
ss := sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherRegexp)
ss.Mock().MatchExpectationsInOrder(false)
alertmanager := alertmanagertest.NewMockAlertmanager(t)
if alertmanagerAlerts == nil {
alertmanagerAlerts = alertmanagertypes.DeprecatedGettableAlerts{}
}
alertmanager.EXPECT().
GetAlerts(mock.Anything, testOrgID.StringValue(), mock.AnythingOfType("alertmanagertypes.GettableAlertsParams")).
Return(alertmanagerAlerts, nil).
Maybe()
return &module{
telemetryStore: ts,
sqlstore: ss,
alertmanager: alertmanager,
licensing: licensing,
}, ts.Mock(), ss.Mock()
}
func expectTelemetryQuiet(mock cmock.ClickConnMockCommon) {
expectCHExists(mock, "signoz_logs.distributed_logs_v2", false)
expectCHExists(mock, "signoz_logs.distributed_logs_v2", false)
expectCHExists(mock, "signoz_traces.distributed_signoz_index_v3", false)
expectCHExists(mock, "signoz_traces.distributed_signoz_index_v3", false)
expectCHExists(mock, "signoz_metrics.distributed_metadata", false)
expectCHExists(mock, "signoz_metrics.distributed_metadata", false)
expectCHExists(mock, "signoz_metrics.distributed_metadata", false)
}
func expectCHExists(mock cmock.ClickConnMockCommon, table string, exists bool) {
row := cmock.NewRow([]cmock.ColumnType{{Name: "exists", Type: "UInt8"}}, nil)
if exists {
row = cmock.NewRow([]cmock.ColumnType{{Name: "exists", Type: "UInt8"}}, []any{uint8(1)})
}
mock.ExpectQueryRow(regexp.QuoteMeta(table)).WillReturnRow(row)
}
func expectSQLCounts(mock sqlmock.Sqlmock, dashboardsCount int, savedViewsCount int, ruleIDs []string) {
ruleRows := mock.NewRows([]string{"id"})
for _, ruleID := range ruleIDs {
ruleRows.AddRow(ruleID)
}
mock.
ExpectQuery(ruleIDsQueryRegex).
WillReturnRows(ruleRows)
mock.
ExpectQuery(dashboardsCountQueryRegex).
WillReturnRows(mock.NewRows([]string{"count"}).AddRow(dashboardsCount))
mock.
ExpectQuery(`SELECT count\(\*\) FROM "saved_views" AS "saved_view" WHERE \(org_id = '` + testOrgID.StringValue() + `'\)`).
WillReturnRows(mock.NewRows([]string{"count"}).AddRow(savedViewsCount))
}

View File

@@ -0,0 +1,41 @@
package implemptystate
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
func (m *module) getDashboardsCount(ctx context.Context, orgID valuer.UUID) (int, error) {
count, err := m.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(new(dashboardtypes.StorableDashboard)).
Where("org_id = ?", orgID.StringValue()).
Where("source = ?", dashboardtypes.SourceUser).
Count(ctx)
if err != nil {
return 0, errors.WrapInternalf(err, errors.CodeInternal, "failed to count dashboards")
}
return count, nil
}
func (m *module) getSavedViewsCount(ctx context.Context, orgID valuer.UUID) (int, error) {
count, err := m.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(new(savedviewtypes.SavedView)).
Where("org_id = ?", orgID.StringValue()).
Count(ctx)
if err != nil {
return 0, errors.WrapInternalf(err, errors.CodeInternal, "failed to count saved views")
}
return count, nil
}

View File

@@ -16,12 +16,7 @@ var hostNameGroupByKey = qbtypes.GroupByKey{
},
}
var hostsTableMetricNamesList = []string{
"system.cpu.time",
"system.memory.usage",
"system.cpu.load_average.15m",
"system.filesystem.usage",
}
var hostsTableMetricNamesList = inframonitoringtypes.HostsTableMetricNames
var hostAttrKeysForMetadata = []string{
"os.type",

View File

@@ -17,16 +17,7 @@ var nodeNameGroupByKey = qbtypes.GroupByKey{
},
}
// nodesTableMetricNamesList drives the existence/retention check.
// Includes condition_ready and pod.phase also.
var nodesTableMetricNamesList = []string{
"k8s.node.cpu.usage",
"k8s.node.allocatable_cpu",
"k8s.node.memory.working_set",
"k8s.node.allocatable_memory",
"k8s.node.condition_ready",
"k8s.pod.phase",
}
var nodesTableMetricNamesList = inframonitoringtypes.NodesTableMetricNames
var nodeAttrKeysForMetadata = []string{
"k8s.node.uid",

View File

@@ -21,15 +21,7 @@ var podUIDGroupByKey = qbtypes.GroupByKey{
},
}
var podsTableMetricNamesList = []string{
"k8s.pod.cpu.usage",
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",
"k8s.pod.memory.working_set",
"k8s.pod.memory_request_utilization",
"k8s.pod.memory_limit_utilization",
"k8s.pod.phase",
}
var podsTableMetricNamesList = inframonitoringtypes.PodsTableMetricNames
var podAttrKeysForMetadata = []string{
"k8s.pod.uid",

View File

@@ -17,8 +17,8 @@ import (
)
const (
signozHistoryDBName = "signoz_analytics"
ruleStateHistoryTableName = "distributed_rule_state_history_v0"
signozHistoryDBName = rulestatehistorytypes.DBName
ruleStateHistoryTableName = rulestatehistorytypes.TableName
)
type store struct {

View File

@@ -18,6 +18,7 @@ import (
"github.com/uptrace/bun"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types"
@@ -46,6 +47,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/app/resource"
"github.com/SigNoz/signoz/pkg/query-service/app/services"
"github.com/SigNoz/signoz/pkg/query-service/app/traces/smart"
"github.com/SigNoz/signoz/pkg/query-service/app/traces/tracedetail"
"github.com/SigNoz/signoz/pkg/query-service/common"
"github.com/SigNoz/signoz/pkg/query-service/constants"
@@ -159,7 +161,9 @@ type ClickHouseReader struct {
traceResourceTableV3 string
traceSummaryTable string
cache cache.Cache
fluxIntervalForTraceDetail time.Duration
cache cache.Cache
cacheForTraceDetail cache.Cache
metadataDB string
metadataTable string
}
@@ -171,6 +175,8 @@ func NewReader(
telemetryStore telemetrystore.TelemetryStore,
prometheus prometheus.Prometheus,
cluster string,
fluxIntervalForTraceDetail time.Duration,
cacheForTraceDetail cache.Cache,
cache cache.Cache,
options *Options,
) *ClickHouseReader {
@@ -218,7 +224,9 @@ func NewReader(
traceTableName: traceTableName,
traceResourceTableV3: options.primary.TraceResourceTableV3,
traceSummaryTable: options.primary.TraceSummaryTable,
cache: cache,
fluxIntervalForTraceDetail: fluxIntervalForTraceDetail,
cache: cache,
cacheForTraceDetail: cacheForTraceDetail,
metadataDB: options.primary.MetadataDB,
metadataTable: options.primary.MetadataTable,
}
@@ -858,6 +866,205 @@ func (r *ClickHouseReader) GetUsage(ctx context.Context, queryParams *model.GetU
return &usageItems, nil
}
func (r *ClickHouseReader) GetSpansForTrace(ctx context.Context, traceID string, traceDetailsQuery string) ([]model.SpanItemV2, *model.ApiError) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalTraces.StringValue(),
instrumentationtypes.CodeNamespace: "clickhouse-reader",
instrumentationtypes.CodeFunctionName: "GetSpansForTrace",
})
var traceSummary model.TraceSummary
summaryQuery := fmt.Sprintf("SELECT trace_id, min(start) AS start, max(end) AS end, sum(num_spans) AS num_spans FROM %s.%s WHERE trace_id=$1 GROUP BY trace_id", r.TraceDB, r.traceSummaryTable)
err := r.db.QueryRow(ctx, summaryQuery, traceID).Scan(&traceSummary.TraceID, &traceSummary.Start, &traceSummary.End, &traceSummary.NumSpans)
if err != nil {
if err == sql.ErrNoRows {
return []model.SpanItemV2{}, nil
}
r.logger.Error("Error in processing trace summary sql query", errorsV2.Attr(err))
return nil, model.ExecutionError(fmt.Errorf("error in processing trace summary sql query: %w", err))
}
var searchScanResponses []model.SpanItemV2
queryStartTime := time.Now()
err = r.db.Select(ctx, &searchScanResponses, traceDetailsQuery, traceID, strconv.FormatInt(traceSummary.Start.Unix()-1800, 10), strconv.FormatInt(traceSummary.End.Unix(), 10))
r.logger.Info(traceDetailsQuery)
if err != nil {
r.logger.Error("Error in processing sql query", errorsV2.Attr(err))
return nil, model.ExecutionError(fmt.Errorf("error in processing trace data sql query: %w", err))
}
r.logger.Info("trace details query took: ", "duration", time.Since(queryStartTime), "traceID", traceID)
return searchScanResponses, nil
}
func (r *ClickHouseReader) GetFlamegraphSpansForTraceCache(ctx context.Context, orgID valuer.UUID, traceID string) (*model.GetFlamegraphSpansForTraceCache, error) {
cachedTraceData := new(model.GetFlamegraphSpansForTraceCache)
err := r.cacheForTraceDetail.Get(ctx, orgID, strings.Join([]string{"getFlamegraphSpansForTrace", traceID}, "-"), cachedTraceData)
if err != nil {
r.logger.Debug("error in retrieving getFlamegraphSpansForTrace cache", errorsV2.Attr(err), "traceID", traceID)
return nil, err
}
if time.Since(time.UnixMilli(int64(cachedTraceData.EndTime))) < r.fluxIntervalForTraceDetail {
r.logger.Info("the trace end time falls under the flux interval, skipping getFlamegraphSpansForTrace cache", "traceID", traceID)
return nil, errors.Errorf("the trace end time falls under the flux interval, skipping getFlamegraphSpansForTrace cache, traceID: %s", traceID)
}
r.logger.Info("cache is successfully hit, applying cache for getFlamegraphSpansForTrace", "traceID", traceID)
return cachedTraceData, nil
}
func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, orgID valuer.UUID, traceID string, req *model.GetFlamegraphSpansForTraceParams) (*model.GetFlamegraphSpansForTraceResponse, error) {
trace := new(model.GetFlamegraphSpansForTraceResponse)
var startTime, endTime, durationNano uint64
var spanIdToSpanNodeMap = map[string]*model.FlamegraphSpan{}
// map[traceID][level]span
var selectedSpans = [][]*model.FlamegraphSpan{}
var traceRoots []*model.FlamegraphSpan
// get the trace tree from cache!
cachedTraceData, err := r.GetFlamegraphSpansForTraceCache(ctx, orgID, traceID)
if err == nil {
startTime = cachedTraceData.StartTime
endTime = cachedTraceData.EndTime
durationNano = cachedTraceData.DurationNano
selectedSpans = cachedTraceData.SelectedSpans
traceRoots = cachedTraceData.TraceRoots
}
if err != nil {
r.logger.Info("cache miss for getFlamegraphSpansForTrace", "traceID", traceID)
selectCols := "timestamp, duration_nano, span_id, trace_id, has_error, links as references, resource_string_service$$name, name, events"
if len(req.SelectFields) > 0 {
selectCols += ", attributes_string, attributes_number, attributes_bool, resources_string"
}
flamegraphQuery := fmt.Sprintf("SELECT %s FROM %s.%s WHERE trace_id=$1 and ts_bucket_start>=$2 and ts_bucket_start<=$3 ORDER BY timestamp ASC, name ASC", selectCols, r.TraceDB, r.traceTableName)
searchScanResponses, err := r.GetSpansForTrace(ctx, traceID, flamegraphQuery)
if err != nil {
return nil, err
}
if len(searchScanResponses) == 0 {
return trace, nil
}
for _, item := range searchScanResponses {
ref := []model.OtelSpanRef{}
err := json.Unmarshal([]byte(item.References), &ref)
if err != nil {
r.logger.Error("Error unmarshalling references", errorsV2.Attr(err))
return nil, errorsV2.Newf(errorsV2.TypeInternal, errorsV2.CodeInternal, "getFlamegraphSpansForTrace: error in unmarshalling references %s", err.Error())
}
events := make([]model.Event, 0)
for _, event := range item.Events {
var eventMap model.Event
err = json.Unmarshal([]byte(event), &eventMap)
if err != nil {
r.logger.Error("Error unmarshalling events", errorsV2.Attr(err))
return nil, errorsV2.Newf(errorsV2.TypeInternal, errorsV2.CodeInternal, "getFlamegraphSpansForTrace: error in unmarshalling events %s", err.Error())
}
events = append(events, eventMap)
}
jsonItem := model.FlamegraphSpan{
SpanID: item.SpanID,
TraceID: item.TraceID,
ServiceName: item.ServiceName,
Name: item.Name,
DurationNano: item.DurationNano,
HasError: item.HasError,
References: ref,
Events: events,
Children: make([]*model.FlamegraphSpan, 0),
}
if len(req.SelectFields) > 0 {
jsonItem.SetRequestedFields(item, req.SelectFields)
}
// metadata calculation
startTimeUnixNano := uint64(item.TimeUnixNano.UnixNano())
if startTime == 0 || startTimeUnixNano < startTime {
startTime = startTimeUnixNano
}
if endTime == 0 || (startTimeUnixNano+jsonItem.DurationNano) > endTime {
endTime = (startTimeUnixNano + jsonItem.DurationNano)
}
if durationNano == 0 || jsonItem.DurationNano > durationNano {
durationNano = jsonItem.DurationNano
}
jsonItem.TimeUnixNano = uint64(item.TimeUnixNano.UnixNano() / 1000000)
spanIdToSpanNodeMap[jsonItem.SpanID] = &jsonItem
}
// traverse through the map and append each node to the children array of the parent node
// and add missing spans
for _, spanNode := range spanIdToSpanNodeMap {
hasParentSpanNode := false
for _, reference := range spanNode.References {
if reference.RefType == "CHILD_OF" && reference.SpanId != "" {
hasParentSpanNode = true
if parentNode, exists := spanIdToSpanNodeMap[reference.SpanId]; exists {
parentNode.Children = append(parentNode.Children, spanNode)
} else {
// insert the missing spans
missingSpan := model.FlamegraphSpan{
SpanID: reference.SpanId,
TraceID: spanNode.TraceID,
ServiceName: "",
Name: "Missing Span",
TimeUnixNano: spanNode.TimeUnixNano,
DurationNano: spanNode.DurationNano,
HasError: false,
Events: make([]model.Event, 0),
Children: make([]*model.FlamegraphSpan, 0),
}
missingSpan.Children = append(missingSpan.Children, spanNode)
spanIdToSpanNodeMap[missingSpan.SpanID] = &missingSpan
traceRoots = append(traceRoots, &missingSpan)
}
}
}
if !hasParentSpanNode && !tracedetail.ContainsFlamegraphSpan(traceRoots, spanNode) {
traceRoots = append(traceRoots, spanNode)
}
}
selectedSpans = tracedetail.GetAllSpansForFlamegraph(traceRoots, spanIdToSpanNodeMap)
// TODO: set the trace data (model.GetFlamegraphSpansForTraceCache) in cache here
// removed existing cache usage since it was not getting used due to this bug https://github.com/SigNoz/engineering-pod/issues/4648
// and was causing out of memory issues https://github.com/SigNoz/engineering-pod/issues/4638
}
processingPostCache := time.Now()
selectedSpansForRequest := selectedSpans
clientLimit := min(req.Limit, tracedetail.MaxLimitWithoutSampling)
totalSpanCount := tracedetail.GetTotalSpanCount(selectedSpans)
if totalSpanCount > uint64(clientLimit) {
// using trace start and end time if boundary ts are set to zero (or not set)
boundaryStart := max(timestamp.MilliToNano(req.BoundaryStartTS), startTime)
boundaryEnd := timestamp.MilliToNano(req.BoundaryEndTS)
if boundaryEnd == 0 {
boundaryEnd = endTime
}
selectedSpansForRequest = tracedetail.GetSelectedSpansForFlamegraphForRequest(req.SelectedSpanID, selectedSpans, boundaryStart, boundaryEnd)
}
r.logger.Debug("getFlamegraphSpansForTrace: processing post cache", "duration", time.Since(processingPostCache), "traceID", traceID, "totalSpans", totalSpanCount, "limit", clientLimit)
trace.Spans = selectedSpansForRequest
trace.StartTimestampMillis = startTime / 1000000
trace.EndTimestampMillis = endTime / 1000000
trace.HasMore = totalSpanCount > uint64(clientLimit)
return trace, nil
}
func (r *ClickHouseReader) GetDependencyGraph(ctx context.Context, queryParams *model.GetServicesParams) (*[]model.ServiceMapDependencyResponseItem, error) {

View File

@@ -534,7 +534,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v2/traces/fields", am.ViewAccess(aH.traceFields)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/traces/fields", am.EditAccess(aH.updateTraceField)).Methods(http.MethodPost)
router.HandleFunc("/api/v2/traces/flamegraph/{traceId}", am.ViewAccess(aH.GetFlamegraphSpansForTrace)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/version", am.OpenAccess(aH.getVersion)).Methods(http.MethodGet)
@@ -1446,6 +1446,40 @@ func (aH *APIHandler) SearchTraces(w http.ResponseWriter, r *http.Request) {
}
func (aH *APIHandler) GetFlamegraphSpansForTrace(w http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, err)
return
}
traceID := mux.Vars(r)["traceId"]
if traceID == "" {
render.Error(w, errors.NewInvalidInputf(errors.CodeInvalidInput, "traceID is required"))
return
}
req := new(model.GetFlamegraphSpansForTraceParams)
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
result, apiErr := aH.reader.GetFlamegraphSpansForTrace(r.Context(), orgID, traceID, req)
if apiErr != nil {
render.Error(w, apiErr)
return
}
aH.WriteJSON(w, r, result)
}
func (aH *APIHandler) listErrors(w http.ResponseWriter, r *http.Request) {
query, err := parseListErrorsRequest(r)

View File

@@ -1409,6 +1409,8 @@ func Test_querier_Traces_runWindowBasedListQueryDesc(t *testing.T) {
telemetryStore,
prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{Timeout: 2 * time.Minute}, telemetryStore),
"",
time.Duration(time.Second),
nil,
nil,
options,
)
@@ -1633,6 +1635,8 @@ func Test_querier_Traces_runWindowBasedListQueryAsc(t *testing.T) {
telemetryStore,
prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{Timeout: 2 * time.Minute}, telemetryStore),
"",
time.Duration(time.Second),
nil,
nil,
options,
)
@@ -1932,6 +1936,8 @@ func Test_querier_Logs_runWindowBasedListQueryDesc(t *testing.T) {
telemetryStore,
prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{Timeout: 2 * time.Minute}, telemetryStore),
"",
time.Duration(time.Second),
nil,
nil,
options,
)
@@ -2158,6 +2164,8 @@ func Test_querier_Logs_runWindowBasedListQueryAsc(t *testing.T) {
telemetryStore,
prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{Timeout: 2 * time.Minute}, telemetryStore),
"",
time.Duration(time.Second),
nil,
nil,
options,
)

View File

@@ -1461,6 +1461,8 @@ func Test_querier_Traces_runWindowBasedListQueryDesc(t *testing.T) {
telemetryStore,
prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{Timeout: 2 * time.Minute}, telemetryStore),
"",
time.Duration(time.Second),
nil,
nil,
options,
)
@@ -1685,6 +1687,8 @@ func Test_querier_Traces_runWindowBasedListQueryAsc(t *testing.T) {
telemetryStore,
prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{Timeout: 2 * time.Minute}, telemetryStore),
"",
time.Duration(time.Second),
nil,
nil,
options,
)
@@ -1983,6 +1987,8 @@ func Test_querier_Logs_runWindowBasedListQueryDesc(t *testing.T) {
telemetryStore,
prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{Timeout: 2 * time.Minute}, telemetryStore),
"",
time.Duration(time.Second),
nil,
nil,
options,
)
@@ -2209,6 +2215,8 @@ func Test_querier_Logs_runWindowBasedListQueryAsc(t *testing.T) {
telemetryStore,
prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{Timeout: 2 * time.Minute}, telemetryStore),
"",
time.Duration(time.Second),
nil,
nil,
options,
)

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"slices"
"github.com/SigNoz/signoz/pkg/cache/memorycache"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/queryparser"
@@ -15,6 +16,7 @@ import (
"github.com/rs/cors"
"github.com/soheilhy/cmux"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
@@ -58,12 +60,25 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
return nil, err
}
cacheForTraceDetail, err := memorycache.New(context.TODO(), signoz.Instrumentation.ToProviderSettings(), cache.Config{
Provider: "memory",
Memory: cache.Memory{
NumCounters: 10 * 10000,
MaxCost: 1 << 27, // 128 MB
},
})
if err != nil {
return nil, err
}
reader := clickhouseReader.NewReader(
signoz.Instrumentation.Logger(),
signoz.SQLStore,
signoz.TelemetryStore,
signoz.Prometheus,
signoz.TelemetryStore.Cluster(),
config.Querier.FluxInterval,
cacheForTraceDetail,
signoz.Cache,
nil,
)

View File

@@ -0,0 +1,199 @@
package tracedetail
import (
"sort"
"github.com/SigNoz/signoz/pkg/query-service/model"
)
var (
flamegraphSpanLevelLimit float64 = 50
flamegraphSpanLimitPerLevel int = 100
flamegraphSamplingBucketCount int = 50
flamegraphTopLatencySpanCount int = 5
MaxLimitWithoutSampling uint = 120_000
)
func ContainsFlamegraphSpan(slice []*model.FlamegraphSpan, item *model.FlamegraphSpan) bool {
for _, v := range slice {
if v.SpanID == item.SpanID {
return true
}
}
return false
}
func BfsTraversalForTrace(span *model.FlamegraphSpan, level int64) map[int64][]*model.FlamegraphSpan {
bfs := map[int64][]*model.FlamegraphSpan{}
bfs[level] = []*model.FlamegraphSpan{span}
for _, child := range span.Children {
childBfsMap := BfsTraversalForTrace(child, level+1)
for _level, nodes := range childBfsMap {
bfs[_level] = append(bfs[_level], nodes...)
}
}
span.Level = level
span.Children = make([]*model.FlamegraphSpan, 0)
return bfs
}
func FindIndexForSelectedSpan(spans [][]*model.FlamegraphSpan, selectedSpanId string) int {
var selectedSpanLevel int = 0
for index, _spans := range spans {
for _, span := range _spans {
if span.SpanID == selectedSpanId {
selectedSpanLevel = index
break
}
}
}
return selectedSpanLevel
}
// GetAllSpansForFlamegraph groups all spans as per their level
func GetAllSpansForFlamegraph(traceRoots []*model.FlamegraphSpan, spanIdToSpanNodeMap map[string]*model.FlamegraphSpan) [][]*model.FlamegraphSpan {
var traceIdLevelledFlamegraph = map[string]map[int64][]*model.FlamegraphSpan{}
selectedSpans := [][]*model.FlamegraphSpan{}
// sort the trace roots to add missing spans at the right order
sort.Slice(traceRoots, func(i, j int) bool {
if traceRoots[i].TimeUnixNano == traceRoots[j].TimeUnixNano {
return traceRoots[i].Name < traceRoots[j].Name
}
return traceRoots[i].TimeUnixNano < traceRoots[j].TimeUnixNano
})
for _, rootSpanID := range traceRoots {
if rootNode, exists := spanIdToSpanNodeMap[rootSpanID.SpanID]; exists {
bfsMapForTrace := BfsTraversalForTrace(rootNode, 0)
traceIdLevelledFlamegraph[rootSpanID.SpanID] = bfsMapForTrace
}
}
for _, trace := range traceRoots {
keys := make([]int64, 0, len(traceIdLevelledFlamegraph[trace.SpanID]))
for key := range traceIdLevelledFlamegraph[trace.SpanID] {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
return keys[i] < keys[j]
})
for _, level := range keys {
if ok, exists := traceIdLevelledFlamegraph[trace.SpanID][level]; exists {
selectedSpans = append(selectedSpans, ok)
}
}
}
return selectedSpans
}
func getLatencyAndTimestampBucketedSpans(spans []*model.FlamegraphSpan, selectedSpanID string, isSelectedSpanIDPresent bool, startTime uint64, endTime uint64) []*model.FlamegraphSpan {
var sampledSpans []*model.FlamegraphSpan
// sort the spans by latency for latency filtering
sort.Slice(spans, func(i, j int) bool {
return spans[i].DurationNano > spans[j].DurationNano
})
// pick the top 5 latency spans
for idx := range flamegraphTopLatencySpanCount {
sampledSpans = append(sampledSpans, spans[idx])
}
// always add the selectedSpan
if isSelectedSpanIDPresent {
idx := -1
for _idx, span := range spans {
if span.SpanID == selectedSpanID {
idx = _idx
}
}
if idx != -1 {
sampledSpans = append(sampledSpans, spans[idx])
}
}
bucketSize := (endTime - startTime) / uint64(flamegraphSamplingBucketCount)
if bucketSize == 0 {
bucketSize = 1
}
bucketedSpans := make([][]*model.FlamegraphSpan, flamegraphSamplingBucketCount)
for _, span := range spans {
if span.TimeUnixNano >= startTime && span.TimeUnixNano <= endTime {
bucketIndex := int((span.TimeUnixNano - startTime) / bucketSize)
if bucketIndex >= 0 && bucketIndex < flamegraphSamplingBucketCount {
bucketedSpans[bucketIndex] = append(bucketedSpans[bucketIndex], span)
}
}
}
for i := range bucketedSpans {
if len(bucketedSpans[i]) > 2 {
// Keep only the first 2 spans
bucketedSpans[i] = bucketedSpans[i][:2]
}
}
// Flatten the bucketed spans into a single slice
for _, bucket := range bucketedSpans {
sampledSpans = append(sampledSpans, bucket...)
}
return sampledSpans
}
func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpans [][]*model.FlamegraphSpan, startTime uint64, endTime uint64) [][]*model.FlamegraphSpan {
var selectedSpansForRequest = make([][]*model.FlamegraphSpan, 0)
var selectedIndex = 0
if selectedSpanID != "" {
selectedIndex = FindIndexForSelectedSpan(selectedSpans, selectedSpanID)
}
lowerLimit := selectedIndex - int(flamegraphSpanLevelLimit*0.4)
upperLimit := selectedIndex + int(flamegraphSpanLevelLimit*0.6)
if lowerLimit < 0 {
upperLimit = upperLimit - lowerLimit
lowerLimit = 0
}
if upperLimit > len(selectedSpans) {
lowerLimit = lowerLimit - (upperLimit - len(selectedSpans))
upperLimit = len(selectedSpans)
}
if lowerLimit < 0 {
lowerLimit = 0
}
for i := lowerLimit; i < upperLimit; i++ {
if len(selectedSpans[i]) > flamegraphSpanLimitPerLevel {
_spans := getLatencyAndTimestampBucketedSpans(selectedSpans[i], selectedSpanID, i == selectedIndex, startTime, endTime)
selectedSpansForRequest = append(selectedSpansForRequest, _spans)
} else {
selectedSpansForRequest = append(selectedSpansForRequest, selectedSpans[i])
}
}
return selectedSpansForRequest
}
func GetTotalSpanCount(spans [][]*model.FlamegraphSpan) uint64 {
levelCount := len(spans)
spanCount := uint64(0)
for i := range levelCount {
spanCount += uint64(len(spans[i]))
}
return spanCount
}

View File

@@ -43,6 +43,8 @@ type Reader interface {
// Search Interfaces
SearchTraces(ctx context.Context, params *model.SearchTracesParams) (*[]model.SearchSpansResult, error)
GetFlamegraphSpansForTrace(ctx context.Context, orgID valuer.UUID, traceID string, req *model.GetFlamegraphSpansForTraceParams) (*model.GetFlamegraphSpansForTraceResponse, error)
// Setter Interfaces
SetTTL(ctx context.Context, orgID string, ttlParams *retentiontypes.TTLParams) (*retentiontypes.SetTTLResponseItem, *model.ApiError)
SetTTLV2(ctx context.Context, orgID string, params *retentiontypes.CustomRetentionTTLParams) (*retentiontypes.CustomRetentionTTLResponse, error)

View File

@@ -0,0 +1,42 @@
package model
import (
"encoding/json"
"github.com/SigNoz/signoz/pkg/types/cachetypes"
)
type GetFlamegraphSpansForTraceCache struct {
StartTime uint64 `json:"startTime"`
EndTime uint64 `json:"endTime"`
DurationNano uint64 `json:"durationNano"`
SelectedSpans [][]*FlamegraphSpan `json:"selectedSpans"`
TraceRoots []*FlamegraphSpan `json:"traceRoots"`
}
func (c *GetFlamegraphSpansForTraceCache) Clone() cachetypes.Cacheable {
return &GetFlamegraphSpansForTraceCache{
StartTime: c.StartTime,
EndTime: c.EndTime,
DurationNano: c.DurationNano,
SelectedSpans: c.SelectedSpans,
TraceRoots: c.TraceRoots,
}
}
func (c *GetFlamegraphSpansForTraceCache) Cost() int64 {
const perSpanBytes = 128
var spans int64
for _, row := range c.SelectedSpans {
spans += int64(len(row))
}
spans += int64(len(c.TraceRoots))
return spans * perSpanBytes
}
func (c *GetFlamegraphSpansForTraceCache) MarshalBinary() (data []byte, err error) {
return json.Marshal(c)
}
func (c *GetFlamegraphSpansForTraceCache) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, c)
}

View File

@@ -2,6 +2,8 @@ package model
import (
"time"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
type InstantQueryMetricsParams struct {
@@ -329,6 +331,14 @@ type SearchTracesParams struct {
MaxSpansInTrace int `json:"maxSpansInTrace"`
}
type GetFlamegraphSpansForTraceParams struct {
SelectedSpanID string `json:"selectedSpanId"`
Limit uint `json:"limit"`
BoundaryStartTS uint64 `json:"boundaryStartTsMilli"`
BoundaryEndTS uint64 `json:"boundarEndTsMilli"`
SelectFields []telemetrytypes.TelemetryFieldKey `json:"selectFields"`
}
type SpanFilterParams struct {
TraceID []string `json:"traceID"`
Status []string `json:"status"`

View File

@@ -7,6 +7,7 @@ import (
"strconv"
"time"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/pkg/errors"
"github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/util/stats"
@@ -239,6 +240,13 @@ type SearchSpanDBResponseItem struct {
Model string `ch:"model"`
}
type Event struct {
Name string `json:"name,omitempty"`
TimeUnixNano uint64 `json:"timeUnixNano,omitempty"`
AttributeMap map[string]interface{} `json:"attributeMap,omitempty"`
IsError bool `json:"isError,omitempty"`
}
//easyjson:json
type SearchSpanResponseItem struct {
TimeUnixNano uint64 `json:"timestamp"`
@@ -259,6 +267,79 @@ type SearchSpanResponseItem struct {
SpanKind string `json:"spanKind"`
}
type Span struct {
TimeUnixNano uint64 `json:"timestamp"`
DurationNano uint64 `json:"durationNano"`
SpanID string `json:"spanId"`
RootSpanID string `json:"rootSpanId"`
TraceID string `json:"traceId"`
HasError bool `json:"hasError"`
Kind int32 `json:"kind"`
ServiceName string `json:"serviceName"`
Name string `json:"name"`
References []OtelSpanRef `json:"references,omitempty"`
TagMap map[string]string `json:"tagMap"`
Events []Event `json:"event"`
RootName string `json:"rootName"`
StatusMessage string `json:"statusMessage"`
StatusCodeString string `json:"statusCodeString"`
SpanKind string `json:"spanKind"`
Children []*Span `json:"children"`
// the below two fields are for frontend to render the spans
SubTreeNodeCount uint64 `json:"subTreeNodeCount"`
HasChildren bool `json:"hasChildren"`
HasSiblings bool `json:"hasSiblings"`
Level uint64 `json:"level"`
}
type FlamegraphSpan struct {
TimeUnixNano uint64 `json:"timestamp"`
DurationNano uint64 `json:"durationNano"`
SpanID string `json:"spanId"`
TraceID string `json:"traceId"`
HasError bool `json:"hasError"`
ServiceName string `json:"serviceName"`
Name string `json:"name"`
Level int64 `json:"level"`
Events []Event `json:"event"`
References []OtelSpanRef `json:"references,omitempty"`
Children []*FlamegraphSpan `json:"children"`
Attributes map[string]any `json:"attributes,omitempty"`
Resource map[string]string `json:"resource,omitempty"`
}
// SetRequestedFields extracts the requested attribute/resource fields from item into s.
// This can eventually support missing fieldContext by checking both
func (s *FlamegraphSpan) SetRequestedFields(item SpanItemV2, fields []telemetrytypes.TelemetryFieldKey) {
for _, field := range fields {
switch field.FieldContext {
case telemetrytypes.FieldContextResource:
if v, ok := item.Resources_string[field.Name]; ok && v != "" {
if s.Resource == nil {
s.Resource = make(map[string]string)
}
s.Resource[field.Name] = v
}
case telemetrytypes.FieldContextAttribute:
if v := item.AttributeValue(field.Name); v != nil {
if s.Attributes == nil {
s.Attributes = make(map[string]any)
}
s.Attributes[field.Name] = v
}
}
}
}
type GetFlamegraphSpansForTraceResponse struct {
StartTimestampMillis uint64 `json:"startTimestampMillis"`
EndTimestampMillis uint64 `json:"endTimestampMillis"`
DurationNano uint64 `json:"durationNano"`
Spans [][]*FlamegraphSpan `json:"spans"`
HasMore bool `json:"hasMore"`
}
type OtelSpanRef struct {
TraceId string `json:"traceId,omitempty"`
SpanId string `json:"spanId,omitempty"`

View File

@@ -18,6 +18,8 @@ import (
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/emptystate"
"github.com/SigNoz/signoz/pkg/modules/emptystate/implemptystate"
"github.com/SigNoz/signoz/pkg/modules/fields"
"github.com/SigNoz/signoz/pkg/modules/fields/implfields"
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
@@ -80,6 +82,7 @@ type Handlers struct {
TraceDetail tracedetail.Handler
RulerHandler ruler.Handler
LLMPricingRuleHandler llmpricingrule.Handler
EmptyState emptystate.Handler
}
func NewHandlers(
@@ -125,5 +128,6 @@ func NewHandlers(
TraceDetail: impltracedetail.NewHandler(modules.TraceDetail),
RulerHandler: signozruler.NewHandler(rulerService),
LLMPricingRuleHandler: impllmpricingrule.NewHandler(modules.LLMPricingRule),
EmptyState: implemptystate.NewHandler(modules.EmptyState),
}
}

View File

@@ -59,7 +59,7 @@ func TestNewHandlers(t *testing.T) {
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), userRoleStore, flagger)
retentionGetter := implretention.NewGetter(implretention.NewStore(sqlstore))
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, nil, nil, retentionGetter, flagger, tagModule)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, nil, nil, retentionGetter, flagger, tagModule)
querierHandler := querier.NewHandler(providerSettings, nil, nil)
registryHandler := factory.NewHandler(nil)

View File

@@ -9,12 +9,15 @@ import (
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/apdex"
"github.com/SigNoz/signoz/pkg/modules/apdex/implapdex"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/authdomain/implauthdomain"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/emptystate"
"github.com/SigNoz/signoz/pkg/modules/emptystate/implemptystate"
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
"github.com/SigNoz/signoz/pkg/modules/inframonitoring/implinframonitoring"
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
@@ -93,6 +96,7 @@ type Modules struct {
SpanMapper spanmapper.Module
LLMPricingRule llmpricingrule.Module
Tag tag.Module
EmptyState emptystate.Module
}
func NewModules(
@@ -102,6 +106,7 @@ func NewModules(
providerSettings factory.ProviderSettings,
orgGetter organization.Getter,
alertmanager alertmanager.Alertmanager,
licensing licensing.Licensing,
analytics analytics.Analytics,
querier querier.Querier,
telemetryStore telemetrystore.TelemetryStore,
@@ -153,5 +158,6 @@ func NewModules(
SpanMapper: implspanmapper.NewModule(implspanmapper.NewStore(sqlstore)),
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore)),
Tag: tagModule,
EmptyState: implemptystate.NewModule(telemetryStore, sqlstore, alertmanager, licensing),
}
}

View File

@@ -63,7 +63,7 @@ func TestNewModules(t *testing.T) {
retentionGetter := implretention.NewGetter(implretention.NewStore(sqlstore))
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, serviceAccount, implcloudintegration.NewModule(), retentionGetter, flagger, tagModule)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, serviceAccount, implcloudintegration.NewModule(), retentionGetter, flagger, tagModule)
reflectVal := reflect.ValueOf(modules)
for i := 0; i < reflectVal.NumField(); i++ {

View File

@@ -20,6 +20,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/emptystate"
"github.com/SigNoz/signoz/pkg/modules/fields"
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
@@ -70,6 +71,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ inframonitoring.Handler }{},
struct{ gateway.Handler }{},
struct{ fields.Handler }{},
struct{ emptystate.Handler }{},
struct{ authz.Handler }{},
struct{ rawdataexport.Handler }{},
struct{ zeus.Handler }{},

View File

@@ -294,6 +294,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.InfraMonitoring,
handlers.GatewayHandler,
handlers.Fields,
handlers.EmptyState,
handlers.AuthzHandler,
handlers.RawDataExport,
handlers.ZeusHandler,

View File

@@ -465,7 +465,7 @@ func New(
}
// Initialize all modules
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, userRoleStore, serviceAccount, cloudIntegrationModule, retentionGetter, flagger, tagModule)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, licensing, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, userRoleStore, serviceAccount, cloudIntegrationModule, retentionGetter, flagger, tagModule)
// Initialize ruler from the variant-specific provider factories
rulerInstance, err := factory.NewProviderFromNamedMap(ctx, providerSettings, config.Ruler, rulerProviderFactories(cache, alertmanager, sqlstore, telemetrystore, telemetryMetadataStore, prometheus, orgGetter, modules.RuleStateHistory, querier, queryParser), "signoz")

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