mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-10 19:00:34 +01:00
Compare commits
1 Commits
feat/trace
...
feat/empty
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e668fd9ee |
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -43,6 +43,7 @@ jobs:
|
||||
- callbackauthn
|
||||
- cloudintegrations
|
||||
- dashboard
|
||||
- emptystate
|
||||
- ingestionkeys
|
||||
- inframonitoring
|
||||
- logspipelines
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -3355,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:
|
||||
@@ -9509,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
|
||||
|
||||
107
frontend/src/api/generated/services/emptystate/index.ts
Normal file
107
frontend/src/api/generated/services/emptystate/index.ts
Normal 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;
|
||||
};
|
||||
@@ -4826,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[] };
|
||||
|
||||
/**
|
||||
@@ -9042,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
|
||||
|
||||
34
pkg/apiserver/signozapiserver/emptystate.go
Normal file
34
pkg/apiserver/signozapiserver/emptystate.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
17
pkg/modules/emptystate/emptystate.go
Normal file
17
pkg/modules/emptystate/emptystate.go
Normal 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)
|
||||
}
|
||||
116
pkg/modules/emptystate/implemptystate/alerts.go
Normal file
116
pkg/modules/emptystate/implemptystate/alerts.go
Normal 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
|
||||
}
|
||||
34
pkg/modules/emptystate/implemptystate/handler.go
Normal file
34
pkg/modules/emptystate/implemptystate/handler.go
Normal 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)
|
||||
}
|
||||
147
pkg/modules/emptystate/implemptystate/ingestion.go
Normal file
147
pkg/modules/emptystate/implemptystate/ingestion.go
Normal 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
|
||||
}
|
||||
204
pkg/modules/emptystate/implemptystate/module.go
Normal file
204
pkg/modules/emptystate/implemptystate/module.go
Normal 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)
|
||||
}
|
||||
367
pkg/modules/emptystate/implemptystate/module_test.go
Normal file
367
pkg/modules/emptystate/implemptystate/module_test.go
Normal 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))
|
||||
}
|
||||
41
pkg/modules/emptystate/implemptystate/resources.go
Normal file
41
pkg/modules/emptystate/implemptystate/resources.go
Normal 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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
signozHistoryDBName = "signoz_analytics"
|
||||
ruleStateHistoryTableName = "distributed_rule_state_history_v0"
|
||||
signozHistoryDBName = rulestatehistorytypes.DBName
|
||||
ruleStateHistoryTableName = rulestatehistorytypes.TableName
|
||||
)
|
||||
|
||||
type store struct {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++ {
|
||||
|
||||
@@ -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 }{},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
36
pkg/types/emptystatetypes/orgcontext.go
Normal file
36
pkg/types/emptystatetypes/orgcontext.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package emptystatetypes
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
HasIngestedDataWindow = 7 * 24 * time.Hour
|
||||
IngestingCurrentlyWindow = time.Hour
|
||||
RecentlyFiredAlertsWindow = 30 * time.Minute
|
||||
)
|
||||
|
||||
type LicenseStatus string
|
||||
|
||||
const LicenseStatusUnknown LicenseStatus = "UNKNOWN"
|
||||
|
||||
func (status LicenseStatus) StringValue() string {
|
||||
return string(status)
|
||||
}
|
||||
|
||||
type OrgContext struct {
|
||||
HasIngestedData bool `json:"hasIngestedData" required:"true"`
|
||||
SignalsIngested SignalsIngested `json:"signalsIngested" required:"true"`
|
||||
IngestingCurrently bool `json:"ingestingCurrently" required:"true"`
|
||||
HasInfraMetrics bool `json:"hasInfraMetrics" required:"true"`
|
||||
AlertsCount int `json:"alertsCount" required:"true"`
|
||||
ActiveFiringAlertsCount int `json:"activeFiringAlertsCount" required:"true"`
|
||||
RecentlyFiredAlertsCount int `json:"recentlyFiredAlertsCount" 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."`
|
||||
}
|
||||
|
||||
type SignalsIngested struct {
|
||||
Logs bool `json:"logs" required:"true"`
|
||||
Traces bool `json:"traces" required:"true"`
|
||||
Metrics bool `json:"metrics" required:"true" description:"Excludes span-generated metrics (signoz_ prefix), which only prove traces ingestion."`
|
||||
}
|
||||
40
pkg/types/emptystatetypes/orgcontext_test.go
Normal file
40
pkg/types/emptystatetypes/orgcontext_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,14 @@ func (HostStatus) Enum() []any {
|
||||
|
||||
const HostNameAttrKey = "host.name"
|
||||
|
||||
// HostsTableMetricNames drives host presence checks (infra monitoring, empty-state).
|
||||
var HostsTableMetricNames = []string{
|
||||
"system.cpu.time",
|
||||
"system.memory.usage",
|
||||
"system.cpu.load_average.15m",
|
||||
"system.filesystem.usage",
|
||||
}
|
||||
|
||||
const (
|
||||
HostsOrderByCPU = "cpu"
|
||||
HostsOrderByMemory = "memory"
|
||||
|
||||
@@ -29,6 +29,16 @@ const (
|
||||
|
||||
const NodeNameAttrKey = "k8s.node.name"
|
||||
|
||||
// NodesTableMetricNames drives node presence checks; includes condition_ready and k8s.pod.phase.
|
||||
var NodesTableMetricNames = []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",
|
||||
}
|
||||
|
||||
const (
|
||||
NodesOrderByCPU = "cpu"
|
||||
NodesOrderByCPUAllocatable = "cpu_allocatable"
|
||||
|
||||
@@ -38,6 +38,17 @@ const (
|
||||
|
||||
const PodNameAttrKey = "k8s.pod.name"
|
||||
|
||||
// PodsTableMetricNames drives pod presence checks (infra monitoring, empty-state).
|
||||
var PodsTableMetricNames = []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",
|
||||
}
|
||||
|
||||
const (
|
||||
PodsOrderByCPU = "cpu"
|
||||
PodsOrderByCPURequest = "cpu_request"
|
||||
|
||||
@@ -13,6 +13,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
DBName = "signoz_analytics"
|
||||
TableName = "distributed_rule_state_history_v0"
|
||||
)
|
||||
|
||||
type LabelsString string
|
||||
|
||||
func (l *LabelsString) Scan(src any) error {
|
||||
|
||||
73
tests/integration/tests/emptystate/01_org_context.py
Normal file
73
tests/integration/tests/emptystate/01_org_context.py
Normal file
@@ -0,0 +1,73 @@
|
||||
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/empty_state/org_context"
|
||||
|
||||
|
||||
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)
|
||||
assert isinstance(data["ingestingCurrently"], bool)
|
||||
assert isinstance(data["hasInfraMetrics"], bool)
|
||||
assert isinstance(data["signalsIngested"]["logs"], bool)
|
||||
assert isinstance(data["signalsIngested"]["traces"], bool)
|
||||
assert isinstance(data["signalsIngested"]["metrics"], bool)
|
||||
assert data["alertsCount"] == 0
|
||||
assert data["activeFiringAlertsCount"] == 0
|
||||
assert data["recentlyFiredAlertsCount"] == 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"
|
||||
Reference in New Issue
Block a user