Compare commits

..

8 Commits

Author SHA1 Message Date
makeavish
772d5a35a2 chore(empty-state): restore nodes metric list comment dropped in revert 2026-06-11 17:35:24 +05:30
makeavish
ee2726ac47 refactor(empty-state): nest endpoint under orgs/me and split statsreporter subpackages 2026-06-11 17:14:41 +05:30
makeavish
9fa3dd6c9b fix(empty-state): preserve telemetry stats best effort 2026-06-11 15:16:34 +05:30
makeavish
1488c81380 fix(empty-state): defer infra metrics context 2026-06-11 15:05:06 +05:30
makeavish
1f7b98b314 test(empty-state): trim redundant org context tests 2026-06-11 13:30:06 +05:30
makeavish
45151d71e7 fix(empty-state): preserve telemetry count queries 2026-06-11 13:23:42 +05:30
makeavish
f2e01cedec fix(empty-state): restore last observed telemetry queries 2026-06-11 13:15:44 +05:30
makeavish
7f6e15cd6e 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-11 12:09:46 +05:30
36 changed files with 1247 additions and 5401 deletions

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

@@ -1364,8 +1364,6 @@ components:
- appservice
- containerapp
- aks
- sqldatabase
- sqldatabasemi
type: string
CloudintegrationtypesServiceMetadata:
properties:
@@ -3357,6 +3355,53 @@ components:
- kind
- spec
type: object
EmptystatetypesLastIngestedAt:
properties:
logs:
description: Null when no logs have been ingested.
format: date-time
nullable: true
type: string
metrics:
description: Null when no metrics have been ingested.
format: date-time
nullable: true
type: string
traces:
description: Null when no traces have been ingested.
format: date-time
nullable: true
type: string
required:
- logs
- traces
- metrics
type: object
EmptystatetypesOrgContext:
properties:
alertsCount:
type: integer
dashboardsCount:
type: integer
hasIngestedData:
type: boolean
lastIngestedAt:
$ref: '#/components/schemas/EmptystatetypesLastIngestedAt'
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
savedViewsCount:
type: integer
required:
- hasIngestedData
- lastIngestedAt
- alertsCount
- dashboardsCount
- savedViewsCount
- licenseStatus
type: object
ErrorsJSON:
properties:
code:
@@ -10448,6 +10493,53 @@ paths:
summary: Update org preference
tags:
- preferences
/api/v1/orgs/me/empty_state:
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/public/dashboards/{id}:
get:
deprecated: false

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/orgs/me/empty_state`,
method: 'GET',
signal,
});
};
export const getGetOrgContextQueryKey = () => {
return [`/api/v1/orgs/me/empty_state`] 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

@@ -2655,8 +2655,6 @@ export enum CloudintegrationtypesServiceIDDTO {
appservice = 'appservice',
containerapp = 'containerapp',
aks = 'aks',
sqldatabase = 'sqldatabase',
sqldatabasemi = 'sqldatabasemi',
}
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**
@@ -4828,6 +4826,52 @@ export enum DashboardtypesVariablePluginKindDTO {
'signoz/QueryVariable' = 'signoz/QueryVariable',
'signoz/CustomVariable' = 'signoz/CustomVariable',
}
export interface EmptystatetypesLastIngestedAtDTO {
/**
* @type string,null
* @format date-time
* @description Null when no logs have been ingested.
*/
logs: string | null;
/**
* @type string,null
* @format date-time
* @description Null when no metrics have been ingested.
*/
metrics: string | null;
/**
* @type string,null
* @format date-time
* @description Null when no traces have been ingested.
*/
traces: string | null;
}
export interface EmptystatetypesOrgContextDTO {
/**
* @type integer
*/
alertsCount: number;
/**
* @type integer
*/
dashboardsCount: number;
/**
* @type boolean
*/
hasIngestedData: boolean;
lastIngestedAt: EmptystatetypesLastIngestedAtDTO;
/**
* @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
*/
savedViewsCount: number;
}
export type FactoryResponseDTOServicesAnyOf = { [key: string]: string[] };
/**
@@ -9283,6 +9327,14 @@ export type GetOrgPreference200 = {
export type UpdateOrgPreferencePathParameters = {
name: string;
};
export type GetOrgContext200 = {
data: EmptystatetypesOrgContextDTO;
/**
* @type string
*/
status: string;
};
export type GetPublicDashboardDataPathParameters = {
id: string;
};

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/orgs/me/empty_state", handler.New(
provider.authzMiddleware.ViewAccess(provider.statsReporterHandler.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

@@ -31,6 +31,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/ruler"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/zeus"
@@ -57,6 +58,7 @@ type provider struct {
infraMonitoringHandler inframonitoring.Handler
gatewayHandler gateway.Handler
fieldsHandler fields.Handler
statsReporterHandler statsreporter.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,
statsReporterHandler statsreporter.Handler,
authzHandler authz.Handler,
rawDataExportHandler rawdataexport.Handler,
zeusHandler zeus.Handler,
@@ -124,6 +127,7 @@ func NewFactory(
infraMonitoringHandler,
gatewayHandler,
fieldsHandler,
statsReporterHandler,
authzHandler,
rawDataExportHandler,
zeusHandler,
@@ -161,6 +165,7 @@ func newProvider(
infraMonitoringHandler inframonitoring.Handler,
gatewayHandler gateway.Handler,
fieldsHandler fields.Handler,
statsReporterHandler statsreporter.Handler,
authzHandler authz.Handler,
rawDataExportHandler rawdataexport.Handler,
zeusHandler zeus.Handler,
@@ -197,6 +202,7 @@ func newProvider(
infraMonitoringHandler: infraMonitoringHandler,
gatewayHandler: gatewayHandler,
fieldsHandler: fieldsHandler,
statsReporterHandler: statsReporterHandler,
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

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><defs><linearGradient id="f67d1585-6164-4ad0-b2dd-f9cc59b2969f" x1="9.908" y1="15.943" x2="7.516" y2="2.383" gradientUnits="userSpaceOnUse"><stop offset="0.15" stop-color="#0078d4"/><stop offset="0.8" stop-color="#5ea0ef"/><stop offset="1" stop-color="#83b9f9"/></linearGradient></defs><g id="a4fd1868-54fe-4ca6-8ff6-3b01866dc27b"><path d="M14.49,7.15A5.147,5.147,0,0,0,9.24,2.164,5.272,5.272,0,0,0,4.216,5.653,4.869,4.869,0,0,0,0,10.4a4.946,4.946,0,0,0,5.068,4.814H13.82A4.292,4.292,0,0,0,18,11.127,4.105,4.105,0,0,0,14.49,7.15Z" fill="url(#f67d1585-6164-4ad0-b2dd-f9cc59b2969f)"/><path d="M12.9,11.4V8H12v4.13h2.46V11.4ZM5.76,9.73a1.825,1.825,0,0,1-.51-.31.441.441,0,0,1-.12-.32.342.342,0,0,1,.15-.3.683.683,0,0,1,.42-.12,1.62,1.62,0,0,1,1,.29V8.11a2.58,2.58,0,0,0-1-.16,1.641,1.641,0,0,0-1.09.34,1.08,1.08,0,0,0-.42.89c0,.51.32.91,1,1.21a2.907,2.907,0,0,1,.62.36.419.419,0,0,1,.15.32.381.381,0,0,1-.16.31.806.806,0,0,1-.45.11,1.66,1.66,0,0,1-1.09-.42V12a2.173,2.173,0,0,0,1.07.24,1.877,1.877,0,0,0,1.18-.33A1.08,1.08,0,0,0,6.84,11a1.048,1.048,0,0,0-.25-.7A2.425,2.425,0,0,0,5.76,9.73ZM11,11.32A2.191,2.191,0,0,0,11,9a1.808,1.808,0,0,0-.7-.75,2,2,0,0,0-1-.26,2.112,2.112,0,0,0-1.08.27A1.856,1.856,0,0,0,7.49,9a2.465,2.465,0,0,0-.26,1.14,2.256,2.256,0,0,0,.24,1,1.766,1.766,0,0,0,.69.74,2.056,2.056,0,0,0,1,.3l.86,1h1.21L10,12.08A1.79,1.79,0,0,0,11,11.32Zm-1-.25a.941.941,0,0,1-.76.35.916.916,0,0,1-.76-.36,1.523,1.523,0,0,1-.29-1,1.529,1.529,0,0,1,.29-1,1,1,0,0,1,.78-.37.869.869,0,0,1,.75.37,1.619,1.619,0,0,1,.27,1A1.459,1.459,0,0,1,10,11.07Z" fill="#f2f2f2"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,565 +0,0 @@
{
"id": "sqldatabase",
"title": "Azure SQL Database",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"supportedSignals": {
"metrics": true,
"logs": true
},
"dataCollected": {
"metrics": [
{
"name": "azure_cpu_percent_average",
"unit": "Percent",
"type": "Gauge",
"description": "Percentage of CPU used by the database workload, relative to its limit."
},
{
"name": "azure_cpu_percent_maximum",
"unit": "Percent",
"type": "Gauge",
"description": "Percentage of CPU used by the database workload, relative to its limit."
},
{
"name": "azure_cpu_percent_minimum",
"unit": "Percent",
"type": "Gauge",
"description": "Percentage of CPU used by the database workload, relative to its limit."
},
{
"name": "azure_sql_instance_cpu_percent_average",
"unit": "Percent",
"type": "Gauge",
"description": "Total CPU usage (user plus system) of the SQL instance, as a percentage."
},
{
"name": "azure_sql_instance_cpu_percent_maximum",
"unit": "Percent",
"type": "Gauge",
"description": "Total CPU usage (user plus system) of the SQL instance, as a percentage."
},
{
"name": "azure_sql_instance_cpu_percent_minimum",
"unit": "Percent",
"type": "Gauge",
"description": "Total CPU usage (user plus system) of the SQL instance, as a percentage."
},
{
"name": "azure_sql_instance_memory_percent_average",
"unit": "Percent",
"type": "Gauge",
"description": "Memory usage of the SQL instance, as a percentage of its limit."
},
{
"name": "azure_sql_instance_memory_percent_maximum",
"unit": "Percent",
"type": "Gauge",
"description": "Memory usage of the SQL instance, as a percentage of its limit."
},
{
"name": "azure_sql_instance_memory_percent_minimum",
"unit": "Percent",
"type": "Gauge",
"description": "Memory usage of the SQL instance, as a percentage of its limit."
},
{
"name": "azure_physical_data_read_percent_average",
"unit": "Percent",
"type": "Gauge",
"description": "Data file IO usage as a percentage of the limit."
},
{
"name": "azure_physical_data_read_percent_maximum",
"unit": "Percent",
"type": "Gauge",
"description": "Data file IO usage as a percentage of the limit."
},
{
"name": "azure_physical_data_read_percent_minimum",
"unit": "Percent",
"type": "Gauge",
"description": "Data file IO usage as a percentage of the limit."
},
{
"name": "azure_log_write_percent_average",
"unit": "Percent",
"type": "Gauge",
"description": "Transaction log write throughput as a percentage of the limit."
},
{
"name": "azure_log_write_percent_maximum",
"unit": "Percent",
"type": "Gauge",
"description": "Transaction log write throughput as a percentage of the limit."
},
{
"name": "azure_log_write_percent_minimum",
"unit": "Percent",
"type": "Gauge",
"description": "Transaction log write throughput as a percentage of the limit."
},
{
"name": "azure_workers_percent_average",
"unit": "Percent",
"type": "Gauge",
"description": "Worker threads in use as a percentage of the limit."
},
{
"name": "azure_workers_percent_maximum",
"unit": "Percent",
"type": "Gauge",
"description": "Worker threads in use as a percentage of the limit."
},
{
"name": "azure_workers_percent_minimum",
"unit": "Percent",
"type": "Gauge",
"description": "Worker threads in use as a percentage of the limit."
},
{
"name": "azure_sessions_percent_average",
"unit": "Percent",
"type": "Gauge",
"description": "Active sessions as a percentage of the limit."
},
{
"name": "azure_sessions_percent_maximum",
"unit": "Percent",
"type": "Gauge",
"description": "Active sessions as a percentage of the limit."
},
{
"name": "azure_sessions_percent_minimum",
"unit": "Percent",
"type": "Gauge",
"description": "Active sessions as a percentage of the limit."
},
{
"name": "azure_sessions_count_average",
"unit": "Count",
"type": "Gauge",
"description": "Number of active sessions."
},
{
"name": "azure_sessions_count_maximum",
"unit": "Count",
"type": "Gauge",
"description": "Number of active sessions."
},
{
"name": "azure_sessions_count_minimum",
"unit": "Count",
"type": "Gauge",
"description": "Number of active sessions."
},
{
"name": "azure_dtu_consumption_percent_average",
"unit": "Percent",
"type": "Gauge",
"description": "DTU consumption as a percentage of the limit (DTU-based purchasing model)."
},
{
"name": "azure_dtu_consumption_percent_maximum",
"unit": "Percent",
"type": "Gauge",
"description": "DTU consumption as a percentage of the limit (DTU-based purchasing model)."
},
{
"name": "azure_dtu_consumption_percent_minimum",
"unit": "Percent",
"type": "Gauge",
"description": "DTU consumption as a percentage of the limit (DTU-based purchasing model)."
},
{
"name": "azure_dtu_used_average",
"unit": "Count",
"type": "Gauge",
"description": "DTUs used (DTU-based purchasing model)."
},
{
"name": "azure_dtu_used_maximum",
"unit": "Count",
"type": "Gauge",
"description": "DTUs used (DTU-based purchasing model)."
},
{
"name": "azure_dtu_used_minimum",
"unit": "Count",
"type": "Gauge",
"description": "DTUs used (DTU-based purchasing model)."
},
{
"name": "azure_dtu_limit_average",
"unit": "Count",
"type": "Gauge",
"description": "DTU limit (DTU-based purchasing model)."
},
{
"name": "azure_dtu_limit_maximum",
"unit": "Count",
"type": "Gauge",
"description": "DTU limit (DTU-based purchasing model)."
},
{
"name": "azure_dtu_limit_minimum",
"unit": "Count",
"type": "Gauge",
"description": "DTU limit (DTU-based purchasing model)."
},
{
"name": "azure_cpu_used_average",
"unit": "Count",
"type": "Gauge",
"description": "vCores used (vCore-based purchasing model)."
},
{
"name": "azure_cpu_used_maximum",
"unit": "Count",
"type": "Gauge",
"description": "vCores used (vCore-based purchasing model)."
},
{
"name": "azure_cpu_used_minimum",
"unit": "Count",
"type": "Gauge",
"description": "vCores used (vCore-based purchasing model)."
},
{
"name": "azure_cpu_limit_average",
"unit": "Count",
"type": "Gauge",
"description": "vCore limit (vCore-based purchasing model)."
},
{
"name": "azure_cpu_limit_maximum",
"unit": "Count",
"type": "Gauge",
"description": "vCore limit (vCore-based purchasing model)."
},
{
"name": "azure_cpu_limit_minimum",
"unit": "Count",
"type": "Gauge",
"description": "vCore limit (vCore-based purchasing model)."
},
{
"name": "azure_storage_average",
"unit": "Bytes",
"type": "Gauge",
"description": "Data space used by the database."
},
{
"name": "azure_storage_maximum",
"unit": "Bytes",
"type": "Gauge",
"description": "Data space used by the database."
},
{
"name": "azure_storage_minimum",
"unit": "Bytes",
"type": "Gauge",
"description": "Data space used by the database."
},
{
"name": "azure_storage_percent_average",
"unit": "Percent",
"type": "Gauge",
"description": "Data space used as a percentage of the maximum data size."
},
{
"name": "azure_storage_percent_maximum",
"unit": "Percent",
"type": "Gauge",
"description": "Data space used as a percentage of the maximum data size."
},
{
"name": "azure_storage_percent_minimum",
"unit": "Percent",
"type": "Gauge",
"description": "Data space used as a percentage of the maximum data size."
},
{
"name": "azure_allocated_data_storage_average",
"unit": "Bytes",
"type": "Gauge",
"description": "Data space allocated to the database (includes unused space)."
},
{
"name": "azure_allocated_data_storage_maximum",
"unit": "Bytes",
"type": "Gauge",
"description": "Data space allocated to the database (includes unused space)."
},
{
"name": "azure_allocated_data_storage_minimum",
"unit": "Bytes",
"type": "Gauge",
"description": "Data space allocated to the database (includes unused space)."
},
{
"name": "azure_xtp_storage_percent_average",
"unit": "Percent",
"type": "Gauge",
"description": "In-Memory OLTP storage used as a percentage of the limit."
},
{
"name": "azure_xtp_storage_percent_maximum",
"unit": "Percent",
"type": "Gauge",
"description": "In-Memory OLTP storage used as a percentage of the limit."
},
{
"name": "azure_xtp_storage_percent_minimum",
"unit": "Percent",
"type": "Gauge",
"description": "In-Memory OLTP storage used as a percentage of the limit."
},
{
"name": "azure_full_backup_size_bytes_average",
"unit": "Bytes",
"type": "Gauge",
"description": "Cumulative full backup storage size."
},
{
"name": "azure_full_backup_size_bytes_maximum",
"unit": "Bytes",
"type": "Gauge",
"description": "Cumulative full backup storage size."
},
{
"name": "azure_full_backup_size_bytes_minimum",
"unit": "Bytes",
"type": "Gauge",
"description": "Cumulative full backup storage size."
},
{
"name": "azure_diff_backup_size_bytes_average",
"unit": "Bytes",
"type": "Gauge",
"description": "Cumulative differential backup storage size."
},
{
"name": "azure_diff_backup_size_bytes_maximum",
"unit": "Bytes",
"type": "Gauge",
"description": "Cumulative differential backup storage size."
},
{
"name": "azure_diff_backup_size_bytes_minimum",
"unit": "Bytes",
"type": "Gauge",
"description": "Cumulative differential backup storage size."
},
{
"name": "azure_log_backup_size_bytes_average",
"unit": "Bytes",
"type": "Gauge",
"description": "Cumulative transaction log backup storage size."
},
{
"name": "azure_log_backup_size_bytes_maximum",
"unit": "Bytes",
"type": "Gauge",
"description": "Cumulative transaction log backup storage size."
},
{
"name": "azure_log_backup_size_bytes_minimum",
"unit": "Bytes",
"type": "Gauge",
"description": "Cumulative transaction log backup storage size."
},
{
"name": "azure_replication_lag_seconds_average",
"unit": "Seconds",
"type": "Gauge",
"description": "Geo-replication lag (RPO) in seconds; reported on the primary database only."
},
{
"name": "azure_replication_lag_seconds_maximum",
"unit": "Seconds",
"type": "Gauge",
"description": "Geo-replication lag (RPO) in seconds; reported on the primary database only."
},
{
"name": "azure_replication_lag_seconds_minimum",
"unit": "Seconds",
"type": "Gauge",
"description": "Geo-replication lag (RPO) in seconds; reported on the primary database only."
},
{
"name": "azure_connection_successful_total",
"unit": "Count",
"type": "Sum",
"description": "Number of successful connections."
},
{
"name": "azure_connection_successful_count",
"unit": "Count",
"type": "Gauge",
"description": "Number of successful connections."
},
{
"name": "azure_connection_failed_total",
"unit": "Count",
"type": "Sum",
"description": "Number of failed connections caused by system errors."
},
{
"name": "azure_connection_failed_count",
"unit": "Count",
"type": "Gauge",
"description": "Number of failed connections caused by system errors."
},
{
"name": "azure_connection_failed_user_error_total",
"unit": "Count",
"type": "Sum",
"description": "Number of failed connections caused by user errors."
},
{
"name": "azure_connection_failed_user_error_count",
"unit": "Count",
"type": "Gauge",
"description": "Number of failed connections caused by user errors."
},
{
"name": "azure_blocked_by_firewall_total",
"unit": "Count",
"type": "Sum",
"description": "Number of connection attempts blocked by the firewall."
},
{
"name": "azure_blocked_by_firewall_count",
"unit": "Count",
"type": "Gauge",
"description": "Number of connection attempts blocked by the firewall."
},
{
"name": "azure_deadlock_total",
"unit": "Count",
"type": "Sum",
"description": "Number of deadlocks."
},
{
"name": "azure_deadlock_count",
"unit": "Count",
"type": "Gauge",
"description": "Number of deadlocks."
},
{
"name": "azure_availability_average",
"unit": "Percent",
"type": "Gauge",
"description": "Database availability percentage (100 if connections succeed, 0 if all fail) per minute."
},
{
"name": "azure_availability_maximum",
"unit": "Percent",
"type": "Gauge",
"description": "Database availability percentage (100 if connections succeed, 0 if all fail) per minute."
},
{
"name": "azure_availability_minimum",
"unit": "Percent",
"type": "Gauge",
"description": "Database availability percentage (100 if connections succeed, 0 if all fail) per minute."
},
{
"name": "azure_availability_count",
"unit": "Percent",
"type": "Gauge",
"description": "Database availability percentage (100 if connections succeed, 0 if all fail) per minute."
},
{
"name": "azure_availability_total",
"unit": "Percent",
"type": "Sum",
"description": "Database availability percentage (100 if connections succeed, 0 if all fail) per minute."
},
{
"name": "azure_tempdb_data_size_average",
"unit": "Count",
"type": "Gauge",
"description": "Space used in tempdb data files, in kilobytes."
},
{
"name": "azure_tempdb_data_size_maximum",
"unit": "Count",
"type": "Gauge",
"description": "Space used in tempdb data files, in kilobytes."
},
{
"name": "azure_tempdb_data_size_minimum",
"unit": "Count",
"type": "Gauge",
"description": "Space used in tempdb data files, in kilobytes."
},
{
"name": "azure_tempdb_log_size_average",
"unit": "Count",
"type": "Gauge",
"description": "Space used in the tempdb transaction log file, in kilobytes."
},
{
"name": "azure_tempdb_log_size_maximum",
"unit": "Count",
"type": "Gauge",
"description": "Space used in the tempdb transaction log file, in kilobytes."
},
{
"name": "azure_tempdb_log_size_minimum",
"unit": "Count",
"type": "Gauge",
"description": "Space used in the tempdb transaction log file, in kilobytes."
},
{
"name": "azure_tempdb_log_used_percent_average",
"unit": "Percent",
"type": "Gauge",
"description": "tempdb transaction log space used as a percentage."
},
{
"name": "azure_tempdb_log_used_percent_maximum",
"unit": "Percent",
"type": "Gauge",
"description": "tempdb transaction log space used as a percentage."
},
{
"name": "azure_tempdb_log_used_percent_minimum",
"unit": "Percent",
"type": "Gauge",
"description": "tempdb transaction log space used as a percentage."
}
],
"logs": [
{
"name": "Resource ID",
"path": "resources.azure.resource.id",
"type": "string"
}
]
},
"telemetryCollectionStrategy": {
"azure": {
"resourceProvider": "Microsoft.Sql",
"resourceType": "servers/databases",
"metrics": {},
"logs": {
"categoryGroups": [
"allLogs"
]
}
}
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "Azure SQL Database Overview",
"description": "Overview of Azure SQL Database metrics",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

@@ -1,7 +0,0 @@
### Monitor Azure SQL Database with SigNoz
Collect key Azure SQL Database (single database) metrics and view them with an out of the box dashboard.
This integration collects platform metrics for the `Microsoft.Sql/servers/databases` resource type.
Note: This integration is for Azure SQL Database (the PaaS offering). Azure SQL Managed Instance and SQL Server on Azure VMs expose a different set of metrics and are not covered here.

View File

@@ -1 +0,0 @@
<svg id="a5c93a83-9fd9-4ccb-ba77-53d6c609a7d3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><defs><linearGradient id="acf2da4b-8aca-4b58-b995-ca6956cf663e" x1="5.41" y1="17.33" x2="5.41" y2="0.61" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#949494"/><stop offset="0.53" stop-color="#a2a2a2"/><stop offset="1" stop-color="#b3b3b3"/></linearGradient><linearGradient id="b152b416-25c2-4a4a-9d7f-a0bb2a13f84a" x1="10.04" y1="17.39" x2="10.04" y2="6.82" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0078d4"/><stop offset="0.16" stop-color="#1380da"/><stop offset="0.53" stop-color="#3c91e5"/><stop offset="0.82" stop-color="#559cec"/><stop offset="1" stop-color="#5ea0ef"/></linearGradient></defs><title>Icon-databases-136</title><path d="M10.32,16.76a.58.58,0,0,1-.57.57H1.07a.57.57,0,0,1-.57-.57V1.18A.56.56,0,0,1,1.07.61H9.75a.57.57,0,0,1,.57.57Z" fill="url(#acf2da4b-8aca-4b58-b995-ca6956cf663e)"/><path d="M1.94,6.47A1.07,1.07,0,0,1,3,5.41H7.9A1.07,1.07,0,0,1,9,6.47H9A1.07,1.07,0,0,1,7.9,7.54H3A1.07,1.07,0,0,1,1.94,6.47Z" fill="#003067"/><path d="M1.94,3.31A1.07,1.07,0,0,1,3,2.24H7.9A1.07,1.07,0,0,1,9,3.31H9A1.07,1.07,0,0,1,7.9,4.37H3A1.07,1.07,0,0,1,1.94,3.31Z" fill="#003067"/><circle cx="3.06" cy="3.31" r="0.72" fill="#50e6ff"/><circle cx="3.06" cy="6.47" r="0.72" fill="#50e6ff"/><path d="M17.5,14.08a3.36,3.36,0,0,0-2.91-3.22,4.22,4.22,0,0,0-4.35-4A4.32,4.32,0,0,0,6.1,9.64a4,4,0,0,0-3.52,3.85,4.06,4.06,0,0,0,4.2,3.9l.37,0H14l.17,0A3.39,3.39,0,0,0,17.5,14.08Z" fill="url(#b152b416-25c2-4a4a-9d7f-a0bb2a13f84a)"/><path d="M13.61,14.45V11.09h-.93v4.12h2.45v-.76ZM6.51,12.8A2.23,2.23,0,0,1,6,12.49a.44.44,0,0,1-.12-.32.34.34,0,0,1,.15-.3.66.66,0,0,1,.42-.12,1.66,1.66,0,0,1,1,.29v-.86a2.89,2.89,0,0,0-1-.15,1.69,1.69,0,0,0-1.09.33,1.1,1.1,0,0,0-.41.89,1.34,1.34,0,0,0,.94,1.2,2.51,2.51,0,0,1,.61.36.42.42,0,0,1,.15.32.34.34,0,0,1-.15.3.75.75,0,0,1-.45.12,1.63,1.63,0,0,1-1.08-.42v.92A2.25,2.25,0,0,0,6,15.28,1.91,1.91,0,0,0,7.15,15a1.07,1.07,0,0,0,.43-.91,1,1,0,0,0-.25-.7A2.36,2.36,0,0,0,6.51,12.8Zm5.16,1.58A2.37,2.37,0,0,0,12,13.12,2.28,2.28,0,0,0,11.75,12a1.77,1.77,0,0,0-.69-.74A1.94,1.94,0,0,0,10,11,2.21,2.21,0,0,0,9,11.29a1.87,1.87,0,0,0-.73.77A2.52,2.52,0,0,0,8,13.2a2.26,2.26,0,0,0,.24,1.05,1.87,1.87,0,0,0,.68.74,2,2,0,0,0,1,.29l.85,1h1.2l-1.19-1.1A1.82,1.82,0,0,0,11.67,14.38Zm-.93-.25a.92.92,0,0,1-.76.35.91.91,0,0,1-.75-.36,1.5,1.5,0,0,1-.28-1,1.46,1.46,0,0,1,.29-1,.92.92,0,0,1,.77-.37.86.86,0,0,1,.74.37,1.54,1.54,0,0,1,.28,1A1.47,1.47,0,0,1,10.74,14.13Z" fill="#f2f2f2"/></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,167 +0,0 @@
{
"id": "sqldatabasemi",
"title": "Azure SQL Database Managed Instance",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"supportedSignals": {
"metrics": true,
"logs": true
},
"dataCollected": {
"metrics": [
{
"name": "azure_avg_cpu_percent_average",
"unit": "Percent",
"type": "Gauge",
"description": "Average CPU utilization of the managed instance, as a percentage."
},
{
"name": "azure_avg_cpu_percent_maximum",
"unit": "Percent",
"type": "Gauge",
"description": "Maximum CPU utilization of the managed instance, as a percentage."
},
{
"name": "azure_avg_cpu_percent_minimum",
"unit": "Percent",
"type": "Gauge",
"description": "Minimum CPU utilization of the managed instance, as a percentage."
},
{
"name": "azure_io_bytes_read_average",
"unit": "Bytes",
"type": "Gauge",
"description": "Average bytes read from storage by the managed instance."
},
{
"name": "azure_io_bytes_read_maximum",
"unit": "Bytes",
"type": "Gauge",
"description": "Maximum bytes read from storage by the managed instance."
},
{
"name": "azure_io_bytes_read_minimum",
"unit": "Bytes",
"type": "Gauge",
"description": "Minimum bytes read from storage by the managed instance."
},
{
"name": "azure_io_bytes_written_average",
"unit": "Bytes",
"type": "Gauge",
"description": "Average bytes written to storage by the managed instance."
},
{
"name": "azure_io_bytes_written_maximum",
"unit": "Bytes",
"type": "Gauge",
"description": "Maximum bytes written to storage by the managed instance."
},
{
"name": "azure_io_bytes_written_minimum",
"unit": "Bytes",
"type": "Gauge",
"description": "Minimum bytes written to storage by the managed instance."
},
{
"name": "azure_io_requests_average",
"unit": "Count",
"type": "Gauge",
"description": "Average number of storage IO requests made by the managed instance."
},
{
"name": "azure_io_requests_maximum",
"unit": "Count",
"type": "Gauge",
"description": "Maximum number of storage IO requests made by the managed instance."
},
{
"name": "azure_io_requests_minimum",
"unit": "Count",
"type": "Gauge",
"description": "Minimum number of storage IO requests made by the managed instance."
},
{
"name": "azure_reserved_storage_mb_average",
"unit": "Count",
"type": "Gauge",
"description": "Average storage space reserved for the managed instance, in megabytes."
},
{
"name": "azure_reserved_storage_mb_maximum",
"unit": "Count",
"type": "Gauge",
"description": "Maximum storage space reserved for the managed instance, in megabytes."
},
{
"name": "azure_reserved_storage_mb_minimum",
"unit": "Count",
"type": "Gauge",
"description": "Minimum storage space reserved for the managed instance, in megabytes."
},
{
"name": "azure_storage_space_used_mb_average",
"unit": "Count",
"type": "Gauge",
"description": "Average storage space used by the managed instance, in megabytes."
},
{
"name": "azure_storage_space_used_mb_maximum",
"unit": "Count",
"type": "Gauge",
"description": "Maximum storage space used by the managed instance, in megabytes."
},
{
"name": "azure_storage_space_used_mb_minimum",
"unit": "Count",
"type": "Gauge",
"description": "Minimum storage space used by the managed instance, in megabytes."
},
{
"name": "azure_virtual_core_count_average",
"unit": "Count",
"type": "Gauge",
"description": "Average number of virtual cores provisioned for the managed instance."
},
{
"name": "azure_virtual_core_count_maximum",
"unit": "Count",
"type": "Gauge",
"description": "Maximum number of virtual cores provisioned for the managed instance."
},
{
"name": "azure_virtual_core_count_minimum",
"unit": "Count",
"type": "Gauge",
"description": "Minimum number of virtual cores provisioned for the managed instance."
}
],
"logs": [
{
"name": "Resource ID",
"path": "resources.azure.resource.id",
"type": "string"
}
]
},
"telemetryCollectionStrategy": {
"azure": {
"resourceProvider": "Microsoft.Sql",
"resourceType": "managedInstances",
"metrics": {},
"logs": {
"categoryGroups": ["allLogs"]
}
}
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "Azure SQL Database Managed Instance Overview",
"description": "Overview of Azure SQL Database Managed Instance metrics",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

@@ -1,5 +0,0 @@
### Monitor Azure SQL Database Managed Instance with SigNoz
Collect key Azure SQL Database Managed Instance metrics and view them with an out of the box dashboard.
This integration collects platform metrics for the `Microsoft.Sql/managedInstances` resource type.

View File

@@ -49,6 +49,9 @@ import (
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/ruler"
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/statsreporter/signozstatsreporterapi"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/zeus"
)
@@ -80,6 +83,7 @@ type Handlers struct {
TraceDetail tracedetail.Handler
RulerHandler ruler.Handler
LLMPricingRuleHandler llmpricingrule.Handler
StatsReporter statsreporter.Handler
}
func NewHandlers(
@@ -97,6 +101,7 @@ func NewHandlers(
registryHandler factory.Handler,
alertmanagerService alertmanager.Alertmanager,
rulerService ruler.Ruler,
telemetryStore telemetrystore.TelemetryStore,
) Handlers {
return Handlers{
SavedView: implsavedview.NewHandler(modules.SavedView),
@@ -125,5 +130,11 @@ func NewHandlers(
TraceDetail: impltracedetail.NewHandler(modules.TraceDetail),
RulerHandler: signozruler.NewHandler(rulerService),
LLMPricingRuleHandler: impllmpricingrule.NewHandler(modules.LLMPricingRule),
StatsReporter: signozstatsreporterapi.NewHandler(telemetryStore, statsreporter.OrgContextCollectors{
Rules: rulerService,
Dashboards: modules.Dashboard,
SavedViews: modules.SavedView,
Licensing: licensing,
}),
}
}

View File

@@ -63,7 +63,7 @@ func TestNewHandlers(t *testing.T) {
querierHandler := querier.NewHandler(providerSettings, nil, nil)
registryHandler := factory.NewHandler(nil)
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler, alertmanager, nil)
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler, alertmanager, nil, nil)
reflectVal := reflect.ValueOf(handlers)
for i := 0; i < reflectVal.NumField(); i++ {
f := reflectVal.Field(i)

View File

@@ -36,6 +36,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/ruler"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/zeus"
"github.com/swaggest/jsonschema-go"
@@ -70,6 +71,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ inframonitoring.Handler }{},
struct{ gateway.Handler }{},
struct{ fields.Handler }{},
struct{ statsreporter.Handler }{},
struct{ authz.Handler }{},
struct{ rawdataexport.Handler }{},
struct{ zeus.Handler }{},

View File

@@ -262,9 +262,9 @@ func NewSharderProviderFactories() factory.NamedMap[factory.ProviderFactory[shar
)
}
func NewStatsReporterProviderFactories(telemetryStore telemetrystore.TelemetryStore, collectors []statsreporter.StatsCollector, orgGetter organization.Getter, userGetter user.Getter, tokenizer tokenizer.Tokenizer, build version.Build, analyticsConfig analytics.Config) factory.NamedMap[factory.ProviderFactory[statsreporter.StatsReporter, statsreporter.Config]] {
func NewStatsReporterProviderFactories(collectors []statsreporter.StatsCollector, orgGetter organization.Getter, userGetter user.Getter, tokenizer tokenizer.Tokenizer, build version.Build, analyticsConfig analytics.Config) factory.NamedMap[factory.ProviderFactory[statsreporter.StatsReporter, statsreporter.Config]] {
return factory.MustNewNamedMap(
analyticsstatsreporter.NewFactory(telemetryStore, collectors, orgGetter, userGetter, tokenizer, build, analyticsConfig),
analyticsstatsreporter.NewFactory(collectors, orgGetter, userGetter, tokenizer, build, analyticsConfig),
noopstatsreporter.NewFactory(),
)
}
@@ -294,6 +294,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.InfraMonitoring,
handlers.GatewayHandler,
handlers.Fields,
handlers.StatsReporter,
handlers.AuthzHandler,
handlers.RawDataExport,
handlers.ZeusHandler,

View File

@@ -85,8 +85,7 @@ func TestNewProviderFactories(t *testing.T) {
userGetter := impluser.NewGetter(impluser.NewStore(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual), instrumentationtest.New().ToProviderSettings()), userRoleStore, flagger)
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual)), nil)
telemetryStore := telemetrystoretest.New(telemetrystore.Config{Provider: "clickhouse"}, sqlmock.QueryMatcherEqual)
NewStatsReporterProviderFactories(telemetryStore, []statsreporter.StatsCollector{}, orgGetter, userGetter, tokenizertest.NewMockTokenizer(t), version.Build{}, analytics.Config{Enabled: true})
NewStatsReporterProviderFactories([]statsreporter.StatsCollector{}, orgGetter, userGetter, tokenizertest.NewMockTokenizer(t), version.Build{}, analytics.Config{Enabled: true})
})
assert.NotPanics(t, func() {

View File

@@ -46,6 +46,7 @@ import (
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/statsreporter/telemetrystatscollector"
"github.com/SigNoz/signoz/pkg/telemetryaudit"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
"github.com/SigNoz/signoz/pkg/telemetrymetadata"
@@ -499,6 +500,7 @@ func New(
serviceAccount,
cloudIntegrationModule,
modules.LogsPipeline,
telemetrystatscollector.New(telemetrystore),
}
// Initialize stats reporter from the available stats reporter provider factories
@@ -506,7 +508,7 @@ func New(
ctx,
providerSettings,
config.StatsReporter,
NewStatsReporterProviderFactories(telemetrystore, statsCollectors, orgGetter, userGetter, tokenizer, version.Info, config.Analytics),
NewStatsReporterProviderFactories(statsCollectors, orgGetter, userGetter, tokenizer, version.Info, config.Analytics),
config.StatsReporter.Provider(),
)
if err != nil {
@@ -535,7 +537,7 @@ func New(
// Initialize all handlers for the modules
registryHandler := factory.NewHandler(registry)
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager, rulerInstance)
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager, rulerInstance, telemetrystore)
// Initialize the API server (after registry so it can access service health)
apiserverInstance, err := factory.NewProviderFromNamedMap(

View File

@@ -16,7 +16,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/tokenizer"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
@@ -32,9 +31,6 @@ type provider struct {
// config
config statsreporter.Config
// used to get telemetry details. srikanthcvv to move this to the querier layer
telemetryStore telemetrystore.TelemetryStore
// a list of collectors, used to collect stats from across the codebase
collectors []statsreporter.StatsCollector
@@ -60,9 +56,9 @@ type provider struct {
stopC chan struct{}
}
func NewFactory(telemetryStore telemetrystore.TelemetryStore, collectors []statsreporter.StatsCollector, orgGetter organization.Getter, userGetter user.Getter, tokenizer tokenizer.Tokenizer, build version.Build, analyticsConfig analytics.Config) factory.ProviderFactory[statsreporter.StatsReporter, statsreporter.Config] {
func NewFactory(collectors []statsreporter.StatsCollector, orgGetter organization.Getter, userGetter user.Getter, tokenizer tokenizer.Tokenizer, build version.Build, analyticsConfig analytics.Config) factory.ProviderFactory[statsreporter.StatsReporter, statsreporter.Config] {
return factory.NewProviderFactory(factory.MustNewName("analytics"), func(ctx context.Context, settings factory.ProviderSettings, config statsreporter.Config) (statsreporter.StatsReporter, error) {
return New(ctx, settings, config, telemetryStore, collectors, orgGetter, userGetter, tokenizer, build, analyticsConfig)
return New(ctx, settings, config, collectors, orgGetter, userGetter, tokenizer, build, analyticsConfig)
})
}
@@ -70,7 +66,6 @@ func New(
ctx context.Context,
providerSettings factory.ProviderSettings,
config statsreporter.Config,
telemetryStore telemetrystore.TelemetryStore,
collectors []statsreporter.StatsCollector,
orgGetter organization.Getter,
userGetter user.Getter,
@@ -86,17 +81,16 @@ func New(
}
return &provider{
settings: settings,
config: config,
telemetryStore: telemetryStore,
collectors: collectors,
orgGetter: orgGetter,
userGetter: userGetter,
analytics: analytics,
tokenizer: tokenizer,
build: build,
deployment: deployment,
stopC: make(chan struct{}),
settings: settings,
config: config,
collectors: collectors,
orgGetter: orgGetter,
userGetter: userGetter,
analytics: analytics,
tokenizer: tokenizer,
build: build,
deployment: deployment,
stopC: make(chan struct{}),
}, nil
}
@@ -235,44 +229,5 @@ func (provider *provider) collectOrg(ctx context.Context, orgID valuer.UUID) map
}
wg.Wait()
var traces uint64
if err := provider.telemetryStore.ClickhouseDB().QueryRow(ctx, "SELECT COUNT(*) FROM signoz_traces.distributed_signoz_index_v3").Scan(&traces); err == nil {
stats["telemetry.traces.count"] = traces
}
var logs uint64
if err := provider.telemetryStore.ClickhouseDB().QueryRow(ctx, "SELECT COUNT(*) FROM signoz_logs.distributed_logs_v2").Scan(&logs); err == nil {
stats["telemetry.logs.count"] = logs
}
var metrics uint64
if err := provider.telemetryStore.ClickhouseDB().QueryRow(ctx, "SELECT COUNT(*) FROM signoz_metrics.distributed_samples_v4").Scan(&metrics); err == nil {
stats["telemetry.metrics.count"] = metrics
}
var tracesLastSeenAt time.Time
if err := provider.telemetryStore.ClickhouseDB().QueryRow(ctx, "SELECT max(timestamp) FROM signoz_traces.distributed_signoz_index_v3").Scan(&tracesLastSeenAt); err == nil {
if tracesLastSeenAt.Unix() != 0 {
stats["telemetry.traces.last_observed.time"] = tracesLastSeenAt.UTC()
stats["telemetry.traces.last_observed.time_unix"] = tracesLastSeenAt.Unix()
}
}
var logsLastSeenAt time.Time
if err := provider.telemetryStore.ClickhouseDB().QueryRow(ctx, "SELECT fromUnixTimestamp64Nano(max(timestamp)) FROM signoz_logs.distributed_logs_v2").Scan(&logsLastSeenAt); err == nil {
if logsLastSeenAt.Unix() != 0 {
stats["telemetry.logs.last_observed.time"] = logsLastSeenAt.UTC()
stats["telemetry.logs.last_observed.time_unix"] = logsLastSeenAt.Unix()
}
}
var metricsLastSeenAt time.Time
if err := provider.telemetryStore.ClickhouseDB().QueryRow(ctx, "SELECT toDateTime(max(unix_milli) / 1000) FROM signoz_metrics.distributed_samples_v4").Scan(&metricsLastSeenAt); err == nil {
if metricsLastSeenAt.Unix() != 0 {
stats["telemetry.metrics.last_observed.time"] = metricsLastSeenAt.UTC()
stats["telemetry.metrics.last_observed.time_unix"] = metricsLastSeenAt.Unix()
}
}
return stats
}

View File

@@ -0,0 +1,35 @@
package signozstatsreporterapi
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type handler struct {
orgContext *orgContext
}
func NewHandler(telemetryStore telemetrystore.TelemetryStore, collectors statsreporter.OrgContextCollectors) statsreporter.Handler {
return &handler{orgContext: newOrgContext(telemetryStore, collectors)}
}
func (h *handler) GetOrgContext(rw http.ResponseWriter, req *http.Request) {
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil {
render.Error(rw, err)
return
}
out, err := h.orgContext.Get(req.Context(), valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, out)
}

View File

@@ -0,0 +1,172 @@
package signozstatsreporterapi
import (
"context"
"strings"
"time"
"golang.org/x/sync/errgroup"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/statsreporter/telemetrystatscollector"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"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/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// orgContext computes the org context signals served by the handler.
type orgContext struct {
telemetryStore telemetrystore.TelemetryStore
collectors statsreporter.OrgContextCollectors
}
func newOrgContext(telemetryStore telemetrystore.TelemetryStore, collectors statsreporter.OrgContextCollectors) *orgContext {
return &orgContext{
telemetryStore: telemetryStore,
collectors: collectors,
}
}
func (c *orgContext) Get(ctx context.Context, orgID valuer.UUID) (*emptystatetypes.OrgContext, error) {
var logsLastIngestedAt *time.Time
var tracesLastIngestedAt *time.Time
var metricsLastIngestedAt *time.Time
var alertsCount int
var dashboardsCount int
var savedViewsCount int
licenseStatus := emptystatetypes.LicenseStatusUnknown
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
lastIngestedAt, err := telemetrystatscollector.LastObservedLogs(gCtx, c.telemetryStore)
if err != nil {
return err
}
logsLastIngestedAt = lastIngestedAt
return nil
})
g.Go(func() error {
lastIngestedAt, err := telemetrystatscollector.LastObservedTraces(gCtx, c.telemetryStore)
if err != nil {
return err
}
tracesLastIngestedAt = lastIngestedAt
return nil
})
g.Go(func() error {
lastIngestedAt, err := telemetrystatscollector.LastObservedMetrics(gCtx, c.telemetryStore)
if err != nil {
return err
}
metricsLastIngestedAt = lastIngestedAt
return nil
})
g.Go(func() error {
var err error
alertsCount, err = c.getCollectedCount(gCtx, c.collectors.Rules, ruletypes.StatKeyRuleCount, orgID)
if err != nil {
return err
}
return nil
})
g.Go(func() error {
var err error
dashboardsCount, err = c.getCollectedCount(gCtx, c.collectors.Dashboards, dashboardtypes.StatKeyDashboardCount, orgID)
if err != nil {
return err
}
return nil
})
g.Go(func() error {
var err error
savedViewsCount, err = c.getCollectedCount(gCtx, c.collectors.SavedViews, savedviewtypes.StatKeySavedViewCount, orgID)
if err != nil {
return err
}
return nil
})
g.Go(func() error {
licenseStatus = c.getLicenseStatus(gCtx, orgID)
return nil
})
if err := g.Wait(); err != nil {
return nil, err
}
lastIngestedAt := emptystatetypes.LastIngestedAt{
Logs: logsLastIngestedAt,
Traces: tracesLastIngestedAt,
Metrics: metricsLastIngestedAt,
}
return &emptystatetypes.OrgContext{
HasIngestedData: lastIngestedAt.Logs != nil || lastIngestedAt.Traces != nil || lastIngestedAt.Metrics != nil,
LastIngestedAt: lastIngestedAt,
AlertsCount: alertsCount,
DashboardsCount: dashboardsCount,
SavedViewsCount: savedViewsCount,
LicenseStatus: licenseStatus,
}, nil
}
func (c *orgContext) getCollectedCount(ctx context.Context, collector statsreporter.StatsCollector, key string, orgID valuer.UUID) (int, error) {
if collector == nil {
return 0, errors.NewInternalf(errors.CodeInternal, "collector for %q is not configured", key)
}
stats, err := collector.Collect(ctx, orgID)
if err != nil {
return 0, errors.WrapInternalf(err, errors.CodeInternal, "failed to collect %q", key)
}
count, ok := stats[key].(int64)
if !ok {
return 0, errors.NewInternalf(errors.CodeInternal, "stat %q is missing from collector output", key)
}
return int(count), nil
}
// License stats degrade to UNKNOWN: community wires nooplicensing (empty stats)
// and a licensing outage must not fail the endpoint.
func (c *orgContext) getLicenseStatus(ctx context.Context, orgID valuer.UUID) emptystatetypes.LicenseStatus {
if c.collectors.Licensing == nil {
return emptystatetypes.LicenseStatusUnknown
}
stats, err := c.collectors.Licensing.Collect(ctx, orgID)
if err != nil {
return emptystatetypes.LicenseStatusUnknown
}
return licenseStatusFromStats(stats)
}
func licenseStatusFromStats(stats map[string]any) emptystatetypes.LicenseStatus {
state, ok := stats[licensetypes.StatKeyLicenseStateName].(string)
if !ok || strings.TrimSpace(state) == "" {
return emptystatetypes.LicenseStatusUnknown
}
// Verbatim passthrough: trimming above is only for blank detection.
return emptystatetypes.LicenseStatus(state)
}

View File

@@ -0,0 +1,266 @@
package signozstatsreporterapi
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/require"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
"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/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var testOrgID = valuer.MustNewUUID("00000000-0000-0000-0000-000000000001")
// Exact SQL telemetrystatscollector must generate; mirrored in its
// collector_test.go so any change to the statements fails both suites.
const (
logsLastObservedSQL = "SELECT fromUnixTimestamp64Nano(max(timestamp)) FROM signoz_logs.distributed_logs_v2"
tracesLastObservedSQL = "SELECT max(timestamp) FROM signoz_traces.distributed_signoz_index_v3"
metricsLastObservedSQL = "SELECT toDateTime(max(unix_milli) / 1000) FROM signoz_metrics.distributed_samples_v4"
)
type fakeCollector struct {
stats map[string]any
err error
}
func (f *fakeCollector) Collect(context.Context, valuer.UUID) (map[string]any, error) {
return f.stats, f.err
}
func okCollectors() statsreporter.OrgContextCollectors {
return statsreporter.OrgContextCollectors{
Rules: &fakeCollector{stats: map[string]any{ruletypes.StatKeyRuleCount: int64(0)}},
Dashboards: &fakeCollector{stats: map[string]any{dashboardtypes.StatKeyDashboardCount: int64(0)}},
SavedViews: &fakeCollector{stats: map[string]any{savedviewtypes.StatKeySavedViewCount: int64(0)}},
Licensing: &fakeCollector{stats: map[string]any{}},
}
}
func TestLicenseStatusFromStats(t *testing.T) {
cases := []struct {
name string
stats map[string]any
want emptystatetypes.LicenseStatus
}{
{
name: "known state passes through",
stats: map[string]any{licensetypes.StatKeyLicenseStateName: "ACTIVATED"},
want: emptystatetypes.LicenseStatus("ACTIVATED"),
},
{
name: "novel state passes through",
stats: map[string]any{licensetypes.StatKeyLicenseStateName: "FUTURE_STATE"},
want: emptystatetypes.LicenseStatus("FUTURE_STATE"),
},
{
name: "missing key returns unknown",
stats: map[string]any{},
want: emptystatetypes.LicenseStatusUnknown,
},
{
name: "blank state returns unknown",
stats: map[string]any{licensetypes.StatKeyLicenseStateName: " "},
want: emptystatetypes.LicenseStatusUnknown,
},
{
name: "non-string state returns unknown",
stats: map[string]any{licensetypes.StatKeyLicenseStateName: 42},
want: emptystatetypes.LicenseStatusUnknown,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
status := licenseStatusFromStats(tc.stats)
assert.Equal(t, tc.want, status)
})
}
}
func TestGetLicenseStatusDegradesToUnknown(t *testing.T) {
t.Run("collector error", func(t *testing.T) {
c := &orgContext{collectors: statsreporter.OrgContextCollectors{Licensing: &fakeCollector{err: assert.AnError}}}
status := c.getLicenseStatus(context.Background(), testOrgID)
assert.Equal(t, emptystatetypes.LicenseStatusUnknown, status)
})
t.Run("nil collector", func(t *testing.T) {
c := &orgContext{}
status := c.getLicenseStatus(context.Background(), testOrgID)
assert.Equal(t, emptystatetypes.LicenseStatusUnknown, status)
})
}
func TestGetCollectedCount(t *testing.T) {
c := &orgContext{}
t.Run("missing key fails", func(t *testing.T) {
_, err := c.getCollectedCount(context.Background(), &fakeCollector{stats: map[string]any{}}, ruletypes.StatKeyRuleCount, testOrgID)
assert.Error(t, err)
})
t.Run("nil collector fails", func(t *testing.T) {
_, err := c.getCollectedCount(context.Background(), nil, ruletypes.StatKeyRuleCount, testOrgID)
assert.Error(t, err)
})
}
func TestGetOrgContextDerivesAggregates(t *testing.T) {
lastIngested := time.Unix(5000, 0).UTC()
cases := []struct {
name string
logs bool
traces bool
metrics bool
}{
{name: "logs only", logs: true},
{name: "traces only", traces: true},
{name: "metrics only", metrics: true},
{name: "nothing ingested"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c, chMock := newOrgContextTest(t, statsreporter.OrgContextCollectors{
Rules: &fakeCollector{stats: map[string]any{ruletypes.StatKeyRuleCount: int64(2)}},
Dashboards: &fakeCollector{stats: map[string]any{dashboardtypes.StatKeyDashboardCount: int64(1)}},
SavedViews: &fakeCollector{stats: map[string]any{savedviewtypes.StatKeySavedViewCount: int64(3)}},
Licensing: &fakeCollector{stats: map[string]any{licensetypes.StatKeyLicenseStateName: "ACTIVATED"}},
})
if tc.logs {
expectLogsLastIngested(chMock, lastIngested)
} else {
expectLogsLastIngested(chMock, time.Time{})
}
if tc.traces {
expectTracesLastIngested(chMock, lastIngested)
} else {
expectTracesLastIngested(chMock, time.Time{})
}
if tc.metrics {
expectMetricsLastIngested(chMock, lastIngested)
} else {
expectMetricsLastIngested(chMock, time.Time{})
}
orgContext, err := c.Get(context.Background(), testOrgID)
require.NoError(t, err)
assert.Equal(t, tc.logs || tc.traces || tc.metrics, orgContext.HasIngestedData)
assertLastIngested(t, tc.logs, orgContext.LastIngestedAt.Logs, lastIngested)
assertLastIngested(t, tc.traces, orgContext.LastIngestedAt.Traces, lastIngested)
assertLastIngested(t, tc.metrics, orgContext.LastIngestedAt.Metrics, lastIngested)
assert.Equal(t, 2, orgContext.AlertsCount)
assert.Equal(t, 1, orgContext.DashboardsCount)
assert.Equal(t, 3, orgContext.SavedViewsCount)
assert.Equal(t, emptystatetypes.LicenseStatus("ACTIVATED"), orgContext.LicenseStatus)
assert.NoError(t, chMock.ExpectationsWereMet())
})
}
}
func assertLastIngested(t *testing.T, ingested bool, got *time.Time, want time.Time) {
t.Helper()
if !ingested {
assert.Nil(t, got)
return
}
require.NotNil(t, got)
assert.Equal(t, want, *got)
}
func TestGetOrgContextLicenseErrorDoesNotFail(t *testing.T) {
collectors := okCollectors()
collectors.Licensing = &fakeCollector{err: assert.AnError}
c, chMock := newOrgContextTest(t, collectors)
expectTelemetryQuiet(chMock)
orgContext, err := c.Get(context.Background(), testOrgID)
require.NoError(t, err)
assert.Equal(t, emptystatetypes.LicenseStatusUnknown, orgContext.LicenseStatus)
assert.NoError(t, chMock.ExpectationsWereMet())
}
func TestGetOrgContextCollectorErrorFails(t *testing.T) {
collectors := okCollectors()
collectors.Rules = &fakeCollector{err: assert.AnError}
c, chMock := newOrgContextTest(t, collectors)
expectTelemetryQuiet(chMock)
orgContext, err := c.Get(context.Background(), testOrgID)
assert.Error(t, err)
assert.Nil(t, orgContext)
}
func TestGetOrgContextClickHouseErrorFails(t *testing.T) {
c, chMock := newOrgContextTest(t, okCollectors())
chMock.ExpectQueryRow(regexp.QuoteMeta(logsLastObservedSQL)).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{{Name: "max(timestamp)", Type: "DateTime64(9)"}}, nil)).
WillReturnError(assert.AnError)
expectTracesLastIngested(chMock, time.Time{})
expectMetricsLastIngested(chMock, time.Time{})
orgContext, err := c.Get(context.Background(), testOrgID)
assert.Error(t, err)
assert.Nil(t, orgContext)
}
func newOrgContextTest(t *testing.T, collectors statsreporter.OrgContextCollectors) (*orgContext, cmock.ClickConnMockCommon) {
t.Helper()
ts := telemetrystoretest.New(telemetrystore.Config{}, sqlmock.QueryMatcherRegexp)
ts.Mock().MatchExpectationsInOrder(false)
return newOrgContext(ts, collectors), ts.Mock()
}
func expectTelemetryQuiet(mock cmock.ClickConnMockCommon) {
expectLogsLastIngested(mock, time.Time{})
expectTracesLastIngested(mock, time.Time{})
expectMetricsLastIngested(mock, time.Time{})
}
func expectLogsLastIngested(mock cmock.ClickConnMockCommon, lastIngestedAt time.Time) {
mock.ExpectQueryRow(regexp.QuoteMeta(logsLastObservedSQL)).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{{Name: "max(timestamp)", Type: "DateTime64(9)"}}, []any{lastIngestedAt}))
}
func expectTracesLastIngested(mock cmock.ClickConnMockCommon, lastIngestedAt time.Time) {
mock.ExpectQueryRow(regexp.QuoteMeta(tracesLastObservedSQL)).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{{Name: "max(timestamp)", Type: "DateTime64(9)"}}, []any{lastIngestedAt}))
}
func expectMetricsLastIngested(mock cmock.ClickConnMockCommon, lastIngestedAt time.Time) {
mock.ExpectQueryRow(regexp.QuoteMeta(metricsLastObservedSQL)).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{{Name: "toDateTime(divide(max(unix_milli), 1000))", Type: "DateTime"}}, []any{lastIngestedAt}))
}

View File

@@ -2,6 +2,7 @@ package statsreporter
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -16,3 +17,15 @@ type StatsReporter interface {
type StatsCollector interface {
Collect(context.Context, valuer.UUID) (map[string]any, error)
}
type Handler interface {
GetOrgContext(rw http.ResponseWriter, req *http.Request)
}
// OrgContextCollectors are the collectors the org context signals are sourced from.
type OrgContextCollectors struct {
Rules StatsCollector
Dashboards StatsCollector
SavedViews StatsCollector
Licensing StatsCollector
}

View File

@@ -0,0 +1,103 @@
// Package telemetrystatscollector implements the telemetry usage StatsCollector
// and exports the shared last-observed ClickHouse queries.
package telemetrystatscollector
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrytraces"
"github.com/SigNoz/signoz/pkg/valuer"
)
// collector collects telemetry usage stats (counts and last-observed times per
// signal). Stats are deployment-scoped since telemetry tables carry no org column.
type collector struct {
telemetryStore telemetrystore.TelemetryStore
}
func New(telemetryStore telemetrystore.TelemetryStore) statsreporter.StatsCollector {
return &collector{telemetryStore: telemetryStore}
}
func (c *collector) Collect(ctx context.Context, _ valuer.UUID) (map[string]any, error) {
stats := make(map[string]any)
if traces, err := c.countRows(ctx, "SELECT COUNT(*) FROM signoz_traces.distributed_signoz_index_v3"); err == nil {
stats["telemetry.traces.count"] = traces
}
if logs, err := c.countRows(ctx, "SELECT COUNT(*) FROM signoz_logs.distributed_logs_v2"); err == nil {
stats["telemetry.logs.count"] = logs
}
if metrics, err := c.countRows(ctx, "SELECT COUNT(*) FROM signoz_metrics.distributed_samples_v4"); err == nil {
stats["telemetry.metrics.count"] = metrics
}
if tracesLastSeenAt, err := LastObservedTraces(ctx, c.telemetryStore); err == nil && tracesLastSeenAt != nil {
stats["telemetry.traces.last_observed.time"] = tracesLastSeenAt.UTC()
stats["telemetry.traces.last_observed.time_unix"] = tracesLastSeenAt.Unix()
}
if logsLastSeenAt, err := LastObservedLogs(ctx, c.telemetryStore); err == nil && logsLastSeenAt != nil {
stats["telemetry.logs.last_observed.time"] = logsLastSeenAt.UTC()
stats["telemetry.logs.last_observed.time_unix"] = logsLastSeenAt.Unix()
}
if metricsLastSeenAt, err := LastObservedMetrics(ctx, c.telemetryStore); err == nil && metricsLastSeenAt != nil {
stats["telemetry.metrics.last_observed.time"] = metricsLastSeenAt.UTC()
stats["telemetry.metrics.last_observed.time_unix"] = metricsLastSeenAt.Unix()
}
return stats, nil
}
func (c *collector) countRows(ctx context.Context, query string) (uint64, error) {
var count uint64
err := c.telemetryStore.ClickhouseDB().QueryRow(ctx, query).Scan(&count)
return count, err
}
// Last-observed queries are deployment-scoped since telemetry tables have no
// org column. Exported because the org context API reuses them per request.
func LastObservedLogs(ctx context.Context, telemetryStore telemetrystore.TelemetryStore) (*time.Time, error) {
query := fmt.Sprintf("SELECT fromUnixTimestamp64Nano(max(timestamp)) FROM %s.%s", telemetrylogs.DBName, telemetrylogs.LogsV2TableName)
return scanLastObserved(ctx, telemetryStore, "logs", query)
}
func LastObservedTraces(ctx context.Context, telemetryStore telemetrystore.TelemetryStore) (*time.Time, error) {
query := fmt.Sprintf("SELECT max(timestamp) FROM %s.%s", telemetrytraces.DBName, telemetrytraces.SpanIndexV3TableName)
return scanLastObserved(ctx, telemetryStore, "traces", query)
}
func LastObservedMetrics(ctx context.Context, telemetryStore telemetrystore.TelemetryStore) (*time.Time, error) {
query := fmt.Sprintf("SELECT toDateTime(max(unix_milli) / 1000) FROM %s.%s", telemetrymetrics.DBName, telemetrymetrics.SamplesV4TableName)
return scanLastObserved(ctx, telemetryStore, "metrics", query)
}
func scanLastObserved(ctx context.Context, telemetryStore telemetrystore.TelemetryStore, signal string, query string, args ...any) (*time.Time, error) {
var lastObserved time.Time
err := telemetryStore.ClickhouseDB().QueryRow(ctx, query, args...).Scan(&lastObserved)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil //nolint:nilnil
}
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to check %s last observed", signal)
}
if lastObserved.Unix() <= 0 {
return nil, nil //nolint:nilnil
}
lastObservedAt := lastObserved.UTC()
return &lastObservedAt, nil
}

View File

@@ -0,0 +1,162 @@
package telemetrystatscollector
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/require"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
"github.com/SigNoz/signoz/pkg/valuer"
)
var testOrgID = valuer.MustNewUUID("00000000-0000-0000-0000-000000000001")
// Exact SQL the query builders must generate; mock expectations match on these
// so any change to the statements fails the suite. The last-observed trio is
// mirrored in signozstatsreporterapi/orgcontext_test.go.
const (
tracesCountSQL = "SELECT COUNT(*) FROM signoz_traces.distributed_signoz_index_v3"
logsCountSQL = "SELECT COUNT(*) FROM signoz_logs.distributed_logs_v2"
metricsCountSQL = "SELECT COUNT(*) FROM signoz_metrics.distributed_samples_v4"
logsLastObservedSQL = "SELECT fromUnixTimestamp64Nano(max(timestamp)) FROM signoz_logs.distributed_logs_v2"
tracesLastObservedSQL = "SELECT max(timestamp) FROM signoz_traces.distributed_signoz_index_v3"
metricsLastObservedSQL = "SELECT toDateTime(max(unix_milli) / 1000) FROM signoz_metrics.distributed_samples_v4"
)
func TestTelemetryStatsCollectorCollect(t *testing.T) {
collector, chMock := newCollectorTest(t)
tracesAt := time.Date(2026, 6, 10, 10, 0, 0, 0, time.UTC)
logsAt := time.Date(2026, 6, 10, 11, 0, 0, 0, time.UTC)
metricsAt := time.Date(2026, 6, 10, 12, 0, 0, 0, time.UTC)
expectTelemetryCount(chMock, tracesCountSQL, 5)
expectTelemetryCount(chMock, logsCountSQL, 7)
expectTelemetryCount(chMock, metricsCountSQL, 9)
expectTracesLastIngested(chMock, tracesAt)
expectLogsLastIngested(chMock, logsAt)
expectMetricsLastIngested(chMock, metricsAt)
stats, err := collector.Collect(context.Background(), testOrgID)
require.NoError(t, err)
assert.Equal(t, uint64(5), stats["telemetry.traces.count"])
assert.Equal(t, uint64(7), stats["telemetry.logs.count"])
assert.Equal(t, uint64(9), stats["telemetry.metrics.count"])
assert.Equal(t, tracesAt, stats["telemetry.traces.last_observed.time"])
assert.Equal(t, tracesAt.Unix(), stats["telemetry.traces.last_observed.time_unix"])
assert.Equal(t, logsAt, stats["telemetry.logs.last_observed.time"])
assert.Equal(t, logsAt.Unix(), stats["telemetry.logs.last_observed.time_unix"])
assert.Equal(t, metricsAt, stats["telemetry.metrics.last_observed.time"])
assert.Equal(t, metricsAt.Unix(), stats["telemetry.metrics.last_observed.time_unix"])
assert.NoError(t, chMock.ExpectationsWereMet())
}
func TestTelemetryStatsCollectorOmitsQuietLastObserved(t *testing.T) {
collector, chMock := newCollectorTest(t)
expectTelemetryCount(chMock, tracesCountSQL, 0)
expectTelemetryCount(chMock, logsCountSQL, 0)
expectTelemetryCount(chMock, metricsCountSQL, 0)
expectTracesLastIngested(chMock, time.Time{})
expectLogsLastIngested(chMock, time.Time{})
expectMetricsLastIngested(chMock, time.Time{})
stats, err := collector.Collect(context.Background(), testOrgID)
require.NoError(t, err)
assert.Equal(t, map[string]any{
"telemetry.traces.count": uint64(0),
"telemetry.logs.count": uint64(0),
"telemetry.metrics.count": uint64(0),
}, stats)
assert.NoError(t, chMock.ExpectationsWereMet())
}
func TestTelemetryStatsCollectorCountErrorOmitsOnlyFailedKey(t *testing.T) {
collector, chMock := newCollectorTest(t)
tracesAt := time.Date(2026, 6, 10, 10, 0, 0, 0, time.UTC)
chMock.ExpectQueryRow(regexp.QuoteMeta(tracesCountSQL)).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{{Name: "count()", Type: "UInt64"}}, nil)).
WillReturnError(assert.AnError)
expectTelemetryCount(chMock, logsCountSQL, 7)
expectTelemetryCount(chMock, metricsCountSQL, 9)
expectTracesLastIngested(chMock, tracesAt)
expectLogsLastIngested(chMock, time.Time{})
expectMetricsLastIngested(chMock, time.Time{})
stats, err := collector.Collect(context.Background(), testOrgID)
require.NoError(t, err)
assert.NotContains(t, stats, "telemetry.traces.count")
assert.Equal(t, uint64(7), stats["telemetry.logs.count"])
assert.Equal(t, uint64(9), stats["telemetry.metrics.count"])
assert.Equal(t, tracesAt, stats["telemetry.traces.last_observed.time"])
assert.NoError(t, chMock.ExpectationsWereMet())
}
func TestTelemetryStatsCollectorLastObservedErrorOmitsOnlyFailedKeys(t *testing.T) {
collector, chMock := newCollectorTest(t)
logsAt := time.Date(2026, 6, 10, 11, 0, 0, 0, time.UTC)
metricsAt := time.Date(2026, 6, 10, 12, 0, 0, 0, time.UTC)
expectTelemetryCount(chMock, tracesCountSQL, 1)
expectTelemetryCount(chMock, logsCountSQL, 1)
expectTelemetryCount(chMock, metricsCountSQL, 1)
chMock.ExpectQueryRow(regexp.QuoteMeta(tracesLastObservedSQL)).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{{Name: "max(timestamp)", Type: "DateTime64(9)"}}, nil)).
WillReturnError(assert.AnError)
expectLogsLastIngested(chMock, logsAt)
expectMetricsLastIngested(chMock, metricsAt)
stats, err := collector.Collect(context.Background(), testOrgID)
require.NoError(t, err)
assert.Equal(t, uint64(1), stats["telemetry.traces.count"])
assert.Equal(t, uint64(1), stats["telemetry.logs.count"])
assert.Equal(t, uint64(1), stats["telemetry.metrics.count"])
assert.NotContains(t, stats, "telemetry.traces.last_observed.time")
assert.NotContains(t, stats, "telemetry.traces.last_observed.time_unix")
assert.Equal(t, logsAt, stats["telemetry.logs.last_observed.time"])
assert.Equal(t, metricsAt, stats["telemetry.metrics.last_observed.time"])
assert.NoError(t, chMock.ExpectationsWereMet())
}
func newCollectorTest(t *testing.T) (statsreporter.StatsCollector, cmock.ClickConnMockCommon) {
t.Helper()
ts := telemetrystoretest.New(telemetrystore.Config{}, sqlmock.QueryMatcherRegexp)
ts.Mock().MatchExpectationsInOrder(false)
return New(ts), ts.Mock()
}
func expectTelemetryCount(mock cmock.ClickConnMockCommon, query string, count uint64) {
mock.ExpectQueryRow(regexp.QuoteMeta(query)).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{{Name: "count()", Type: "UInt64"}}, []any{count}))
}
func expectLogsLastIngested(mock cmock.ClickConnMockCommon, lastIngestedAt time.Time) {
mock.ExpectQueryRow(regexp.QuoteMeta(logsLastObservedSQL)).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{{Name: "max(timestamp)", Type: "DateTime64(9)"}}, []any{lastIngestedAt}))
}
func expectTracesLastIngested(mock cmock.ClickConnMockCommon, lastIngestedAt time.Time) {
mock.ExpectQueryRow(regexp.QuoteMeta(tracesLastObservedSQL)).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{{Name: "max(timestamp)", Type: "DateTime64(9)"}}, []any{lastIngestedAt}))
}
func expectMetricsLastIngested(mock cmock.ClickConnMockCommon, lastIngestedAt time.Time) {
mock.ExpectQueryRow(regexp.QuoteMeta(metricsLastObservedSQL)).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{{Name: "toDateTime(divide(max(unix_milli), 1000))", Type: "DateTime"}}, []any{lastIngestedAt}))
}

View File

@@ -25,14 +25,12 @@ var (
AWSServiceSQS = ServiceID{valuer.NewString("sqs")}
// Azure services.
AzureServiceStorageAccountsBlob = ServiceID{valuer.NewString("storageaccountsblob")}
AzureServiceCDNProfile = ServiceID{valuer.NewString("cdnprofile")}
AzureServiceVirtualMachine = ServiceID{valuer.NewString("virtualmachine")}
AzureServiceAppService = ServiceID{valuer.NewString("appservice")}
AzureServiceContainerApp = ServiceID{valuer.NewString("containerapp")}
AzureServiceAKS = ServiceID{valuer.NewString("aks")}
AzureServiceSQLDatabase = ServiceID{valuer.NewString("sqldatabase")}
AzureServiceSQLDatabaseManagedInstance = ServiceID{valuer.NewString("sqldatabasemi")}
AzureServiceStorageAccountsBlob = ServiceID{valuer.NewString("storageaccountsblob")}
AzureServiceCDNProfile = ServiceID{valuer.NewString("cdnprofile")}
AzureServiceVirtualMachine = ServiceID{valuer.NewString("virtualmachine")}
AzureServiceAppService = ServiceID{valuer.NewString("appservice")}
AzureServiceContainerApp = ServiceID{valuer.NewString("containerapp")}
AzureServiceAKS = ServiceID{valuer.NewString("aks")}
)
func (ServiceID) Enum() []any {
@@ -56,8 +54,6 @@ func (ServiceID) Enum() []any {
AzureServiceAppService,
AzureServiceContainerApp,
AzureServiceAKS,
AzureServiceSQLDatabase,
AzureServiceSQLDatabaseManagedInstance,
}
}
@@ -85,8 +81,6 @@ var SupportedServices = map[CloudProviderType][]ServiceID{
AzureServiceAppService,
AzureServiceContainerApp,
AzureServiceAKS,
AzureServiceSQLDatabase,
AzureServiceSQLDatabaseManagedInstance,
},
}

View File

@@ -168,6 +168,9 @@ func NewGettableDashboardFromDashboard(dashboard *Dashboard) (*GettableDashboard
}, nil
}
// StatKeyDashboardCount is the dashboard count stat key shared by the stats reporter and its API consumers.
const StatKeyDashboardCount = "dashboard.count"
func NewStatsFromStorableDashboards(dashboards []*StorableDashboard) map[string]any {
stats := make(map[string]any)
stats["dashboard.panels.count"] = int64(0)
@@ -178,7 +181,7 @@ func NewStatsFromStorableDashboards(dashboards []*StorableDashboard) map[string]
addStatsFromStorableDashboard(dashboard, stats)
}
stats["dashboard.count"] = int64(len(dashboards))
stats[StatKeyDashboardCount] = int64(len(dashboards))
return stats
}

View File

@@ -0,0 +1,27 @@
package emptystatetypes
import "time"
type LicenseStatus string
const LicenseStatusUnknown LicenseStatus = "UNKNOWN"
func (status LicenseStatus) StringValue() string {
return string(status)
}
type OrgContext struct {
HasIngestedData bool `json:"hasIngestedData" required:"true"`
LastIngestedAt LastIngestedAt `json:"lastIngestedAt" required:"true"`
AlertsCount int `json:"alertsCount" required:"true"`
DashboardsCount int `json:"dashboardsCount" required:"true"`
SavedViewsCount int `json:"savedViewsCount" required:"true"`
LicenseStatus LicenseStatus `json:"licenseStatus" required:"true" 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."`
}
// LastIngestedAt carries per-signal latest ingest times; null means no data has been observed.
type LastIngestedAt struct {
Logs *time.Time `json:"logs" required:"true" description:"Null when no logs have been ingested."`
Traces *time.Time `json:"traces" required:"true" description:"Null when no traces have been ingested."`
Metrics *time.Time `json:"metrics" required:"true" description:"Null when no metrics have been ingested."`
}

View File

@@ -0,0 +1,40 @@
package emptystatetypes
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLicenseStatusMarshalJSON(t *testing.T) {
tests := []struct {
name string
status LicenseStatus
want string
}{
{
name: "known zeus state preserves case",
status: LicenseStatus("ACTIVATED"),
want: `"ACTIVATED"`,
},
{
name: "unknown sentinel preserves case",
status: LicenseStatusUnknown,
want: `"UNKNOWN"`,
},
{
name: "novel state passes through",
status: LicenseStatus("FUTURE_STATE"),
want: `"FUTURE_STATE"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := json.Marshal(tt.status)
assert.NoError(t, err)
assert.Equal(t, tt.want, string(got))
})
}
}

View File

@@ -358,11 +358,14 @@ func NewLicenseFromStorableLicense(storableLicense *StorableLicense) (*License,
}
// StatKeyLicenseStateName is the license state stat key shared by the stats reporter and its API consumers.
const StatKeyLicenseStateName = "license.state.name"
func NewStatsFromLicense(license *License) map[string]any {
return map[string]any{
"license.id": license.ID.StringValue(),
"license.plan.name": license.PlanName.StringValue(),
"license.state.name": license.State,
StatKeyLicenseStateName: license.State,
"license.free_until.time": license.FreeUntil.UTC(),
}
}

View File

@@ -20,6 +20,9 @@ type StorableRule struct {
OrgID string `bun:"org_id,type:text"`
}
// StatKeyRuleCount is the rule count stat key shared by the stats reporter and its API consumers.
const StatKeyRuleCount = "rule.count"
func NewStatsFromRules(rules []*StorableRule) map[string]any {
stats := make(map[string]any)
for _, rule := range rules {
@@ -43,7 +46,7 @@ func NewStatsFromRules(rules []*StorableRule) map[string]any {
}
}
stats["rule.count"] = int64(len(rules))
stats[StatKeyRuleCount] = int64(len(rules))
return stats
}

View File

@@ -22,6 +22,9 @@ type SavedView struct {
ExtraData string `json:"extraData" bun:"extra_data,type:text"`
}
// StatKeySavedViewCount is the saved view count stat key shared by the stats reporter and its API consumers.
const StatKeySavedViewCount = "savedview.count"
func NewStatsFromSavedViews(savedViews []*SavedView) map[string]any {
stats := make(map[string]any)
for _, savedView := range savedViews {
@@ -33,6 +36,6 @@ func NewStatsFromSavedViews(savedViews []*SavedView) map[string]any {
}
}
stats["savedview.count"] = int64(len(savedViews))
stats[StatKeySavedViewCount] = int64(len(savedViews))
return stats
}

View File

@@ -0,0 +1,71 @@
from collections.abc import Callable
from http import HTTPStatus
import requests
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
ORG_CONTEXT_PATH = "/api/v1/orgs/me/empty_state"
def test_get_org_context_unauthenticated(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
):
response = requests.get(
signoz.self.host_configs["8080"].get(ORG_CONTEXT_PATH),
timeout=2,
)
assert response.status_code == HTTPStatus.UNAUTHORIZED
def test_get_org_context_without_license(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get(ORG_CONTEXT_PATH),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
data = response.json()["data"]
assert isinstance(data["hasIngestedData"], bool)
# Fresh stack has no telemetry, so every last-ingested timestamp is null.
assert data["lastIngestedAt"]["logs"] is None
assert data["lastIngestedAt"]["traces"] is None
assert data["lastIngestedAt"]["metrics"] is None
assert data["hasIngestedData"] is False
assert data["alertsCount"] == 0
assert data["dashboardsCount"] == 0
assert data["savedViewsCount"] == 0
# No license registered yet, so the sentinel is returned.
assert data["licenseStatus"] == "UNKNOWN"
def test_get_org_context_with_license(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
apply_license: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get(ORG_CONTEXT_PATH),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK
# The Zeus mock issues a license with state EVALUATING; the API passes it
# through verbatim rather than mapping it to a trial/paid vocabulary.
assert response.json()["data"]["licenseStatus"] == "EVALUATING"