Compare commits

...

6 Commits

Author SHA1 Message Date
Srikanth Chekuri
10135d86f8 Fix timeline default limit and escape exists key (#10490)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-03-04 23:34:21 +05:30
srikanthccv
21bab3ba72 chore: generate 2026-03-04 22:51:37 +05:30
srikanthccv
099f451cf7 Merge branch 'rule-state-history' of github.com:SigNoz/signoz into rule-state-history 2026-03-04 22:26:09 +05:30
srikanthccv
9da95b231d chore: run generate 2026-03-04 22:21:48 +05:30
Srikanth Chekuri
4df40a81f2 Merge branch 'main' into rule-state-history 2026-03-04 21:02:39 +05:30
srikanthccv
73a76b8974 chore: add rule state history module 2026-03-04 20:42:19 +05:30
25 changed files with 3612 additions and 203 deletions

View File

@@ -1763,6 +1763,140 @@ components:
- type
- orgId
type: object
RulestatehistorytypesAlertState:
enum:
- inactive
- pending
- recovering
- firing
- nodata
- disabled
type: string
RulestatehistorytypesRuleStateHistoryContributorResponse:
properties:
count:
minimum: 0
type: integer
fingerprint:
minimum: 0
type: integer
labels:
items:
$ref: '#/components/schemas/Querybuildertypesv5Label'
nullable: true
type: array
relatedLogsLink:
type: string
relatedTracesLink:
type: string
required:
- fingerprint
- labels
- count
type: object
RulestatehistorytypesRuleStateHistoryResponseItem:
properties:
fingerprint:
minimum: 0
type: integer
labels:
items:
$ref: '#/components/schemas/Querybuildertypesv5Label'
nullable: true
type: array
overallState:
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
overallStateChanged:
type: boolean
ruleID:
type: string
ruleName:
type: string
state:
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
stateChanged:
type: boolean
unixMilli:
format: int64
type: integer
value:
format: double
type: number
required:
- ruleID
- ruleName
- overallState
- overallStateChanged
- state
- stateChanged
- unixMilli
- labels
- fingerprint
- value
type: object
RulestatehistorytypesRuleStateTimelineResponse:
properties:
items:
items:
$ref: '#/components/schemas/RulestatehistorytypesRuleStateHistoryResponseItem'
nullable: true
type: array
nextCursor:
type: string
total:
minimum: 0
type: integer
required:
- items
- total
type: object
RulestatehistorytypesRuleStateWindow:
properties:
end:
format: int64
type: integer
start:
format: int64
type: integer
state:
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
required:
- state
- start
- end
type: object
RulestatehistorytypesStats:
properties:
currentAvgResolutionTime:
format: double
type: number
currentAvgResolutionTimeSeries:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
currentTriggersSeries:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
pastAvgResolutionTime:
format: double
type: number
pastAvgResolutionTimeSeries:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
pastTriggersSeries:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
totalCurrentTriggers:
minimum: 0
type: integer
totalPastTriggers:
minimum: 0
type: integer
required:
- totalCurrentTriggers
- totalPastTriggers
- currentTriggersSeries
- pastTriggersSeries
- currentAvgResolutionTime
- pastAvgResolutionTime
- currentAvgResolutionTimeSeries
- pastAvgResolutionTimeSeries
type: object
ServiceaccounttypesFactorAPIKey:
properties:
createdAt:
@@ -6818,6 +6952,518 @@ paths:
summary: Update my organization
tags:
- orgs
/api/v2/rules/{id}/history/filter_keys:
get:
deprecated: false
description: Returns distinct label keys from rule history entries for the selected
range.
operationId: GetRuleHistoryFilterKeys
parameters:
- in: query
name: signal
schema:
$ref: '#/components/schemas/TelemetrytypesSignal'
- in: query
name: source
schema:
$ref: '#/components/schemas/TelemetrytypesSource'
- in: query
name: limit
schema:
type: integer
- in: query
name: startUnixMilli
schema:
format: int64
type: integer
- in: query
name: endUnixMilli
schema:
format: int64
type: integer
- in: query
name: fieldContext
schema:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
- in: query
name: fieldDataType
schema:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
- in: query
name: metricName
schema:
type: string
- in: query
name: searchText
schema:
type: string
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TelemetrytypesGettableFieldKeys'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get rule history filter keys
tags:
- rules
/api/v2/rules/{id}/history/filter_values:
get:
deprecated: false
description: Returns distinct label values for a given key from rule history
entries.
operationId: GetRuleHistoryFilterValues
parameters:
- in: query
name: signal
schema:
$ref: '#/components/schemas/TelemetrytypesSignal'
- in: query
name: source
schema:
$ref: '#/components/schemas/TelemetrytypesSource'
- in: query
name: limit
schema:
type: integer
- in: query
name: startUnixMilli
schema:
format: int64
type: integer
- in: query
name: endUnixMilli
schema:
format: int64
type: integer
- in: query
name: fieldContext
schema:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
- in: query
name: fieldDataType
schema:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
- in: query
name: metricName
schema:
type: string
- in: query
name: searchText
schema:
type: string
- in: query
name: name
schema:
type: string
- in: query
name: existingQuery
schema:
type: string
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TelemetrytypesGettableFieldValues'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get rule history filter values
tags:
- rules
/api/v2/rules/{id}/history/overall_status:
get:
deprecated: false
description: Returns overall firing/inactive intervals for a rule in the selected
time range.
operationId: GetRuleHistoryOverallStatus
parameters:
- in: query
name: start
required: true
schema:
format: int64
type: integer
- in: query
name: end
required: true
schema:
format: int64
type: integer
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/RulestatehistorytypesRuleStateWindow'
nullable: true
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get rule overall status timeline
tags:
- rules
/api/v2/rules/{id}/history/stats:
get:
deprecated: false
description: Returns trigger and resolution statistics for a rule in the selected
time range.
operationId: GetRuleHistoryStats
parameters:
- in: query
name: start
required: true
schema:
format: int64
type: integer
- in: query
name: end
required: true
schema:
format: int64
type: integer
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/RulestatehistorytypesStats'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get rule history stats
tags:
- rules
/api/v2/rules/{id}/history/timeline:
get:
deprecated: false
description: Returns paginated timeline entries for rule state transitions.
operationId: GetRuleHistoryTimeline
parameters:
- in: query
name: start
required: true
schema:
format: int64
type: integer
- in: query
name: end
required: true
schema:
format: int64
type: integer
- in: query
name: state
schema:
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
- in: query
name: filterExpression
schema:
type: string
- in: query
name: limit
schema:
format: int64
type: integer
- in: query
name: order
schema:
$ref: '#/components/schemas/Querybuildertypesv5OrderDirection'
- in: query
name: cursor
schema:
type: string
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/RulestatehistorytypesRuleStateTimelineResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get rule history timeline
tags:
- rules
/api/v2/rules/{id}/history/top_contributors:
get:
deprecated: false
description: Returns top label combinations contributing to rule firing in the
selected time range.
operationId: GetRuleHistoryTopContributors
parameters:
- in: query
name: start
required: true
schema:
format: int64
type: integer
- in: query
name: end
required: true
schema:
format: int64
type: integer
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/RulestatehistorytypesRuleStateHistoryContributorResponse'
nullable: true
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get top contributors to rule firing
tags:
- rules
/api/v2/sessions:
delete:
deprecated: false

View File

@@ -25,6 +25,7 @@ import (
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/signoz"
@@ -102,6 +103,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
signoz.TelemetryMetadataStore,
signoz.Prometheus,
signoz.Modules.OrgGetter,
signoz.Modules.RuleStateHistory,
signoz.Querier,
signoz.Instrumentation.ToProviderSettings(),
signoz.QueryParser,
@@ -349,29 +351,30 @@ func (s *Server) Stop(ctx context.Context) error {
return nil
}
func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, ruleStateHistoryModule rulestatehistory.Module, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
// create manager opts
managerOpts := &baserules.ManagerOptions{
TelemetryStore: telemetryStore,
MetadataStore: metadataStore,
Prometheus: prometheus,
Context: context.Background(),
Logger: zap.L(),
Reader: ch,
Querier: querier,
SLogger: providerSettings.Logger,
Cache: cache,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,
PrepareTestRuleFunc: rules.TestNotification,
Alertmanager: alertmanager,
OrgGetter: orgGetter,
RuleStore: ruleStore,
MaintenanceStore: maintenanceStore,
SqlStore: sqlstore,
QueryParser: queryParser,
TelemetryStore: telemetryStore,
MetadataStore: metadataStore,
Prometheus: prometheus,
Context: context.Background(),
Logger: zap.L(),
Reader: ch,
Querier: querier,
SLogger: providerSettings.Logger,
Cache: cache,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,
PrepareTestRuleFunc: rules.TestNotification,
Alertmanager: alertmanager,
OrgGetter: orgGetter,
RuleStore: ruleStore,
MaintenanceStore: maintenanceStore,
SqlStore: sqlstore,
QueryParser: queryParser,
RuleStateHistoryModule: ruleStateHistoryModule,
}
// create Manager

View File

@@ -26,6 +26,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
}
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
// create a threshold rule
tr, err := baserules.NewThresholdRule(
@@ -39,6 +40,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
@@ -63,6 +65,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
@@ -88,6 +91,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
return task, err

View File

@@ -0,0 +1,744 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
QueryClient,
QueryFunction,
QueryKey,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useQuery } from 'react-query';
import type { ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
GetRuleHistoryFilterKeys200,
GetRuleHistoryFilterKeysParams,
GetRuleHistoryFilterKeysPathParameters,
GetRuleHistoryFilterValues200,
GetRuleHistoryFilterValuesParams,
GetRuleHistoryFilterValuesPathParameters,
GetRuleHistoryOverallStatus200,
GetRuleHistoryOverallStatusParams,
GetRuleHistoryOverallStatusPathParameters,
GetRuleHistoryStats200,
GetRuleHistoryStatsParams,
GetRuleHistoryStatsPathParameters,
GetRuleHistoryTimeline200,
GetRuleHistoryTimelineParams,
GetRuleHistoryTimelinePathParameters,
GetRuleHistoryTopContributors200,
GetRuleHistoryTopContributorsParams,
GetRuleHistoryTopContributorsPathParameters,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
/**
* Returns distinct label keys from rule history entries for the selected range.
* @summary Get rule history filter keys
*/
export const getRuleHistoryFilterKeys = (
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryFilterKeys200>({
url: `/api/v2/rules/${id}/history/filter_keys`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryFilterKeysQueryKey = (
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
) => {
return [
`/api/v2/rules/${id}/history/filter_keys`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryFilterKeysQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetRuleHistoryFilterKeysQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>
> = ({ signal }) => getRuleHistoryFilterKeys({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryFilterKeysQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>
>;
export type GetRuleHistoryFilterKeysQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule history filter keys
*/
export function useGetRuleHistoryFilterKeys<
TData = Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryFilterKeysQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule history filter keys
*/
export const invalidateGetRuleHistoryFilterKeys = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryFilterKeysQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns distinct label values for a given key from rule history entries.
* @summary Get rule history filter values
*/
export const getRuleHistoryFilterValues = (
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryFilterValues200>({
url: `/api/v2/rules/${id}/history/filter_values`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryFilterValuesQueryKey = (
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
) => {
return [
`/api/v2/rules/${id}/history/filter_values`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryFilterValuesQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetRuleHistoryFilterValuesQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>
> = ({ signal }) => getRuleHistoryFilterValues({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryFilterValuesQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>
>;
export type GetRuleHistoryFilterValuesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule history filter values
*/
export function useGetRuleHistoryFilterValues<
TData = Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryFilterValuesQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule history filter values
*/
export const invalidateGetRuleHistoryFilterValues = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryFilterValuesQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns overall firing/inactive intervals for a rule in the selected time range.
* @summary Get rule overall status timeline
*/
export const getRuleHistoryOverallStatus = (
{ id }: GetRuleHistoryOverallStatusPathParameters,
params: GetRuleHistoryOverallStatusParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryOverallStatus200>({
url: `/api/v2/rules/${id}/history/overall_status`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryOverallStatusQueryKey = (
{ id }: GetRuleHistoryOverallStatusPathParameters,
params?: GetRuleHistoryOverallStatusParams,
) => {
return [
`/api/v2/rules/${id}/history/overall_status`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryOverallStatusQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryOverallStatusPathParameters,
params: GetRuleHistoryOverallStatusParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetRuleHistoryOverallStatusQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>
> = ({ signal }) => getRuleHistoryOverallStatus({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryOverallStatusQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>
>;
export type GetRuleHistoryOverallStatusQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule overall status timeline
*/
export function useGetRuleHistoryOverallStatus<
TData = Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryOverallStatusPathParameters,
params: GetRuleHistoryOverallStatusParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryOverallStatusQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule overall status timeline
*/
export const invalidateGetRuleHistoryOverallStatus = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryOverallStatusPathParameters,
params: GetRuleHistoryOverallStatusParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryOverallStatusQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns trigger and resolution statistics for a rule in the selected time range.
* @summary Get rule history stats
*/
export const getRuleHistoryStats = (
{ id }: GetRuleHistoryStatsPathParameters,
params: GetRuleHistoryStatsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryStats200>({
url: `/api/v2/rules/${id}/history/stats`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryStatsQueryKey = (
{ id }: GetRuleHistoryStatsPathParameters,
params?: GetRuleHistoryStatsParams,
) => {
return [
`/api/v2/rules/${id}/history/stats`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryStatsQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryStatsPathParameters,
params: GetRuleHistoryStatsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetRuleHistoryStatsQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryStats>>
> = ({ signal }) => getRuleHistoryStats({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryStatsQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryStats>>
>;
export type GetRuleHistoryStatsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule history stats
*/
export function useGetRuleHistoryStats<
TData = Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryStatsPathParameters,
params: GetRuleHistoryStatsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryStatsQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule history stats
*/
export const invalidateGetRuleHistoryStats = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryStatsPathParameters,
params: GetRuleHistoryStatsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryStatsQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns paginated timeline entries for rule state transitions.
* @summary Get rule history timeline
*/
export const getRuleHistoryTimeline = (
{ id }: GetRuleHistoryTimelinePathParameters,
params: GetRuleHistoryTimelineParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryTimeline200>({
url: `/api/v2/rules/${id}/history/timeline`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryTimelineQueryKey = (
{ id }: GetRuleHistoryTimelinePathParameters,
params?: GetRuleHistoryTimelineParams,
) => {
return [
`/api/v2/rules/${id}/history/timeline`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryTimelineQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryTimelinePathParameters,
params: GetRuleHistoryTimelineParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetRuleHistoryTimelineQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>
> = ({ signal }) => getRuleHistoryTimeline({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryTimelineQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>
>;
export type GetRuleHistoryTimelineQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule history timeline
*/
export function useGetRuleHistoryTimeline<
TData = Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryTimelinePathParameters,
params: GetRuleHistoryTimelineParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryTimelineQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule history timeline
*/
export const invalidateGetRuleHistoryTimeline = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryTimelinePathParameters,
params: GetRuleHistoryTimelineParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryTimelineQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns top label combinations contributing to rule firing in the selected time range.
* @summary Get top contributors to rule firing
*/
export const getRuleHistoryTopContributors = (
{ id }: GetRuleHistoryTopContributorsPathParameters,
params: GetRuleHistoryTopContributorsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryTopContributors200>({
url: `/api/v2/rules/${id}/history/top_contributors`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryTopContributorsQueryKey = (
{ id }: GetRuleHistoryTopContributorsPathParameters,
params?: GetRuleHistoryTopContributorsParams,
) => {
return [
`/api/v2/rules/${id}/history/top_contributors`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryTopContributorsQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryTopContributorsPathParameters,
params: GetRuleHistoryTopContributorsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetRuleHistoryTopContributorsQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>
> = ({ signal }) => getRuleHistoryTopContributors({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryTopContributorsQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>
>;
export type GetRuleHistoryTopContributorsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get top contributors to rule firing
*/
export function useGetRuleHistoryTopContributors<
TData = Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryTopContributorsPathParameters,
params: GetRuleHistoryTopContributorsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryTopContributorsQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get top contributors to rule firing
*/
export const invalidateGetRuleHistoryTopContributors = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryTopContributorsPathParameters,
params: GetRuleHistoryTopContributorsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryTopContributorsQueryKey({ id }, params) },
options,
);
return queryClient;
};

View File

@@ -2090,6 +2090,139 @@ export interface RoletypesRoleDTO {
updatedAt?: Date;
}
export enum RulestatehistorytypesAlertStateDTO {
inactive = 'inactive',
pending = 'pending',
recovering = 'recovering',
firing = 'firing',
nodata = 'nodata',
disabled = 'disabled',
}
export interface RulestatehistorytypesRuleStateHistoryContributorResponseDTO {
/**
* @type integer
* @minimum 0
*/
count: number;
/**
* @type integer
* @minimum 0
*/
fingerprint: number;
/**
* @type array
* @nullable true
*/
labels: Querybuildertypesv5LabelDTO[] | null;
/**
* @type string
*/
relatedLogsLink?: string;
/**
* @type string
*/
relatedTracesLink?: string;
}
export interface RulestatehistorytypesRuleStateHistoryResponseItemDTO {
/**
* @type integer
* @minimum 0
*/
fingerprint: number;
/**
* @type array
* @nullable true
*/
labels: Querybuildertypesv5LabelDTO[] | null;
overallState: RulestatehistorytypesAlertStateDTO;
/**
* @type boolean
*/
overallStateChanged: boolean;
/**
* @type string
*/
ruleID: string;
/**
* @type string
*/
ruleName: string;
state: RulestatehistorytypesAlertStateDTO;
/**
* @type boolean
*/
stateChanged: boolean;
/**
* @type integer
* @format int64
*/
unixMilli: number;
/**
* @type number
* @format double
*/
value: number;
}
export interface RulestatehistorytypesRuleStateTimelineResponseDTO {
/**
* @type array
* @nullable true
*/
items: RulestatehistorytypesRuleStateHistoryResponseItemDTO[] | null;
/**
* @type string
*/
nextCursor?: string;
/**
* @type integer
* @minimum 0
*/
total: number;
}
export interface RulestatehistorytypesRuleStateWindowDTO {
/**
* @type integer
* @format int64
*/
end: number;
/**
* @type integer
* @format int64
*/
start: number;
state: RulestatehistorytypesAlertStateDTO;
}
export interface RulestatehistorytypesStatsDTO {
/**
* @type number
* @format double
*/
currentAvgResolutionTime: number;
currentAvgResolutionTimeSeries: Querybuildertypesv5TimeSeriesDTO;
currentTriggersSeries: Querybuildertypesv5TimeSeriesDTO;
/**
* @type number
* @format double
*/
pastAvgResolutionTime: number;
pastAvgResolutionTimeSeries: Querybuildertypesv5TimeSeriesDTO;
pastTriggersSeries: Querybuildertypesv5TimeSeriesDTO;
/**
* @type integer
* @minimum 0
*/
totalCurrentTriggers: number;
/**
* @type integer
* @minimum 0
*/
totalPastTriggers: number;
}
export interface ServiceaccounttypesFactorAPIKeyDTO {
/**
* @type string
@@ -3563,6 +3696,266 @@ export type GetMyOrganization200 = {
status: string;
};
export type GetRuleHistoryFilterKeysPathParameters = {
id: string;
};
export type GetRuleHistoryFilterKeysParams = {
/**
* @description undefined
*/
signal?: TelemetrytypesSignalDTO;
/**
* @description undefined
*/
source?: TelemetrytypesSourceDTO;
/**
* @type integer
* @description undefined
*/
limit?: number;
/**
* @type integer
* @format int64
* @description undefined
*/
startUnixMilli?: number;
/**
* @type integer
* @format int64
* @description undefined
*/
endUnixMilli?: number;
/**
* @description undefined
*/
fieldContext?: TelemetrytypesFieldContextDTO;
/**
* @description undefined
*/
fieldDataType?: TelemetrytypesFieldDataTypeDTO;
/**
* @type string
* @description undefined
*/
metricName?: string;
/**
* @type string
* @description undefined
*/
searchText?: string;
};
export type GetRuleHistoryFilterKeys200 = {
data: TelemetrytypesGettableFieldKeysDTO;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryFilterValuesPathParameters = {
id: string;
};
export type GetRuleHistoryFilterValuesParams = {
/**
* @description undefined
*/
signal?: TelemetrytypesSignalDTO;
/**
* @description undefined
*/
source?: TelemetrytypesSourceDTO;
/**
* @type integer
* @description undefined
*/
limit?: number;
/**
* @type integer
* @format int64
* @description undefined
*/
startUnixMilli?: number;
/**
* @type integer
* @format int64
* @description undefined
*/
endUnixMilli?: number;
/**
* @description undefined
*/
fieldContext?: TelemetrytypesFieldContextDTO;
/**
* @description undefined
*/
fieldDataType?: TelemetrytypesFieldDataTypeDTO;
/**
* @type string
* @description undefined
*/
metricName?: string;
/**
* @type string
* @description undefined
*/
searchText?: string;
/**
* @type string
* @description undefined
*/
name?: string;
/**
* @type string
* @description undefined
*/
existingQuery?: string;
};
export type GetRuleHistoryFilterValues200 = {
data: TelemetrytypesGettableFieldValuesDTO;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryOverallStatusPathParameters = {
id: string;
};
export type GetRuleHistoryOverallStatusParams = {
/**
* @type integer
* @format int64
* @description undefined
*/
start: number;
/**
* @type integer
* @format int64
* @description undefined
*/
end: number;
};
export type GetRuleHistoryOverallStatus200 = {
/**
* @type array
* @nullable true
*/
data: RulestatehistorytypesRuleStateWindowDTO[] | null;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryStatsPathParameters = {
id: string;
};
export type GetRuleHistoryStatsParams = {
/**
* @type integer
* @format int64
* @description undefined
*/
start: number;
/**
* @type integer
* @format int64
* @description undefined
*/
end: number;
};
export type GetRuleHistoryStats200 = {
data: RulestatehistorytypesStatsDTO;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryTimelinePathParameters = {
id: string;
};
export type GetRuleHistoryTimelineParams = {
/**
* @type integer
* @format int64
* @description undefined
*/
start: number;
/**
* @type integer
* @format int64
* @description undefined
*/
end: number;
/**
* @description undefined
*/
state?: RulestatehistorytypesAlertStateDTO;
/**
* @type string
* @description undefined
*/
filterExpression?: string;
/**
* @type integer
* @format int64
* @description undefined
*/
limit?: number;
/**
* @description undefined
*/
order?: Querybuildertypesv5OrderDirectionDTO;
/**
* @type string
* @description undefined
*/
cursor?: string;
};
export type GetRuleHistoryTimeline200 = {
data: RulestatehistorytypesRuleStateTimelineResponseDTO;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryTopContributorsPathParameters = {
id: string;
};
export type GetRuleHistoryTopContributorsParams = {
/**
* @type integer
* @format int64
* @description undefined
*/
start: number;
/**
* @type integer
* @format int64
* @description undefined
*/
end: number;
};
export type GetRuleHistoryTopContributors200 = {
/**
* @type array
* @nullable true
*/
data: RulestatehistorytypesRuleStateHistoryContributorResponseDTO[] | null;
/**
* @type string
*/
status: string;
};
export type GetSessionContext200 = {
data: AuthtypesSessionContextDTO;
/**

View File

@@ -18,6 +18,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
@@ -29,27 +30,28 @@ import (
)
type provider struct {
config apiserver.Config
settings factory.ScopedProviderSettings
router *mux.Router
authZ *middleware.AuthZ
orgHandler organization.Handler
userHandler user.Handler
sessionHandler session.Handler
authDomainHandler authdomain.Handler
preferenceHandler preference.Handler
globalHandler global.Handler
promoteHandler promote.Handler
flaggerHandler flagger.Handler
dashboardModule dashboard.Module
dashboardHandler dashboard.Handler
metricsExplorerHandler metricsexplorer.Handler
gatewayHandler gateway.Handler
fieldsHandler fields.Handler
authzHandler authz.Handler
zeusHandler zeus.Handler
querierHandler querier.Handler
serviceAccountHandler serviceaccount.Handler
config apiserver.Config
settings factory.ScopedProviderSettings
router *mux.Router
authZ *middleware.AuthZ
orgHandler organization.Handler
userHandler user.Handler
sessionHandler session.Handler
authDomainHandler authdomain.Handler
preferenceHandler preference.Handler
globalHandler global.Handler
promoteHandler promote.Handler
flaggerHandler flagger.Handler
dashboardModule dashboard.Module
dashboardHandler dashboard.Handler
metricsExplorerHandler metricsexplorer.Handler
gatewayHandler gateway.Handler
fieldsHandler fields.Handler
authzHandler authz.Handler
zeusHandler zeus.Handler
querierHandler querier.Handler
serviceAccountHandler serviceaccount.Handler
ruleStateHistoryHandler rulestatehistory.Handler
}
func NewFactory(
@@ -72,6 +74,7 @@ func NewFactory(
zeusHandler zeus.Handler,
querierHandler querier.Handler,
serviceAccountHandler serviceaccount.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
return newProvider(
@@ -97,6 +100,7 @@ func NewFactory(
zeusHandler,
querierHandler,
serviceAccountHandler,
ruleStateHistoryHandler,
)
})
}
@@ -124,31 +128,33 @@ func newProvider(
zeusHandler zeus.Handler,
querierHandler querier.Handler,
serviceAccountHandler serviceaccount.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
provider := &provider{
config: config,
settings: settings,
router: router,
orgHandler: orgHandler,
userHandler: userHandler,
sessionHandler: sessionHandler,
authDomainHandler: authDomainHandler,
preferenceHandler: preferenceHandler,
globalHandler: globalHandler,
promoteHandler: promoteHandler,
flaggerHandler: flaggerHandler,
dashboardModule: dashboardModule,
dashboardHandler: dashboardHandler,
metricsExplorerHandler: metricsExplorerHandler,
gatewayHandler: gatewayHandler,
fieldsHandler: fieldsHandler,
authzHandler: authzHandler,
zeusHandler: zeusHandler,
querierHandler: querierHandler,
serviceAccountHandler: serviceAccountHandler,
config: config,
settings: settings,
router: router,
orgHandler: orgHandler,
userHandler: userHandler,
sessionHandler: sessionHandler,
authDomainHandler: authDomainHandler,
preferenceHandler: preferenceHandler,
globalHandler: globalHandler,
promoteHandler: promoteHandler,
flaggerHandler: flaggerHandler,
dashboardModule: dashboardModule,
dashboardHandler: dashboardHandler,
metricsExplorerHandler: metricsExplorerHandler,
gatewayHandler: gatewayHandler,
fieldsHandler: fieldsHandler,
authzHandler: authzHandler,
zeusHandler: zeusHandler,
querierHandler: querierHandler,
serviceAccountHandler: serviceAccountHandler,
ruleStateHistoryHandler: ruleStateHistoryHandler,
}
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
@@ -233,6 +239,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addRuleStateHistoryRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,118 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/gorilla/mux"
)
func (provider *provider) addRuleStateHistoryRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/rules/{id}/history/stats", handler.New(
provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryStats),
handler.OpenAPIDef{
ID: "GetRuleHistoryStats",
Tags: []string{"rules"},
Summary: "Get rule history stats",
Description: "Returns trigger and resolution statistics for a rule in the selected time range.",
RequestQuery: new(rulestatehistorytypes.V2HistoryBaseQueryParams),
Response: new(rulestatehistorytypes.Stats),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rules/{id}/history/timeline", handler.New(
provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryTimeline),
handler.OpenAPIDef{
ID: "GetRuleHistoryTimeline",
Tags: []string{"rules"},
Summary: "Get rule history timeline",
Description: "Returns paginated timeline entries for rule state transitions.",
RequestQuery: new(rulestatehistorytypes.V2HistoryTimelineQueryParams),
Response: new(rulestatehistorytypes.RuleStateTimelineResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rules/{id}/history/top_contributors", handler.New(
provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryContributors),
handler.OpenAPIDef{
ID: "GetRuleHistoryTopContributors",
Tags: []string{"rules"},
Summary: "Get top contributors to rule firing",
Description: "Returns top label combinations contributing to rule firing in the selected time range.",
RequestQuery: new(rulestatehistorytypes.V2HistoryBaseQueryParams),
Response: new([]rulestatehistorytypes.RuleStateHistoryContributorResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rules/{id}/history/filter_keys", handler.New(
provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryFilterKeys),
handler.OpenAPIDef{
ID: "GetRuleHistoryFilterKeys",
Tags: []string{"rules"},
Summary: "Get rule history filter keys",
Description: "Returns distinct label keys from rule history entries for the selected range.",
RequestQuery: new(telemetrytypes.PostableFieldKeysParams),
Response: new(telemetrytypes.GettableFieldKeys),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rules/{id}/history/filter_values", handler.New(
provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryFilterValues),
handler.OpenAPIDef{
ID: "GetRuleHistoryFilterValues",
Tags: []string{"rules"},
Summary: "Get rule history filter values",
Description: "Returns distinct label values for a given key from rule history entries.",
RequestQuery: new(telemetrytypes.PostableFieldValueParams),
Response: new(telemetrytypes.GettableFieldValues),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rules/{id}/history/overall_status", handler.New(
provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryOverallStatus),
handler.OpenAPIDef{
ID: "GetRuleHistoryOverallStatus",
Tags: []string{"rules"},
Summary: "Get rule overall status timeline",
Description: "Returns overall firing/inactive intervals for a rule in the selected time range.",
RequestQuery: new(rulestatehistorytypes.V2HistoryBaseQueryParams),
Response: new([]rulestatehistorytypes.RuleStateWindow),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,106 @@
package implrulestatehistory
import (
"context"
"fmt"
"slices"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querybuilder"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/huandu/go-sqlbuilder"
)
type conditionBuilder struct {
fm qbtypes.FieldMapper
}
func newConditionBuilder(fm qbtypes.FieldMapper) qbtypes.ConditionBuilder {
return &conditionBuilder{fm: fm}
}
func (c *conditionBuilder) ConditionFor(
ctx context.Context,
key *telemetrytypes.TelemetryFieldKey,
operator qbtypes.FilterOperator,
value any,
sb *sqlbuilder.SelectBuilder,
_ uint64,
_ uint64,
) (string, error) {
if operator.IsStringSearchOperator() {
value = querybuilder.FormatValueForContains(value)
}
fieldName, err := c.fm.FieldFor(ctx, key)
if err != nil {
return "", err
}
switch operator {
case qbtypes.FilterOperatorEqual:
return sb.E(fieldName, value), nil
case qbtypes.FilterOperatorNotEqual:
return sb.NE(fieldName, value), nil
case qbtypes.FilterOperatorGreaterThan:
return sb.G(fieldName, value), nil
case qbtypes.FilterOperatorGreaterThanOrEq:
return sb.GE(fieldName, value), nil
case qbtypes.FilterOperatorLessThan:
return sb.LT(fieldName, value), nil
case qbtypes.FilterOperatorLessThanOrEq:
return sb.LE(fieldName, value), nil
case qbtypes.FilterOperatorLike:
return sb.Like(fieldName, value), nil
case qbtypes.FilterOperatorNotLike:
return sb.NotLike(fieldName, value), nil
case qbtypes.FilterOperatorILike:
return sb.ILike(fieldName, value), nil
case qbtypes.FilterOperatorNotILike:
return sb.NotILike(fieldName, value), nil
case qbtypes.FilterOperatorContains:
return sb.ILike(fieldName, fmt.Sprintf("%%%s%%", value)), nil
case qbtypes.FilterOperatorNotContains:
return sb.NotILike(fieldName, fmt.Sprintf("%%%s%%", value)), nil
case qbtypes.FilterOperatorRegexp:
return fmt.Sprintf(`match(%s, %s)`, sqlbuilder.Escape(fieldName), sb.Var(value)), nil
case qbtypes.FilterOperatorNotRegexp:
return fmt.Sprintf(`NOT match(%s, %s)`, sqlbuilder.Escape(fieldName), sb.Var(value)), nil
case qbtypes.FilterOperatorBetween:
values, ok := value.([]any)
if !ok || len(values) != 2 {
return "", qbtypes.ErrBetweenValues
}
return sb.Between(fieldName, values[0], values[1]), nil
case qbtypes.FilterOperatorNotBetween:
values, ok := value.([]any)
if !ok || len(values) != 2 {
return "", qbtypes.ErrBetweenValues
}
return sb.NotBetween(fieldName, values[0], values[1]), nil
case qbtypes.FilterOperatorIn:
values, ok := value.([]any)
if !ok {
return "", qbtypes.ErrInValues
}
return sb.In(fieldName, values), nil
case qbtypes.FilterOperatorNotIn:
values, ok := value.([]any)
if !ok {
return "", qbtypes.ErrInValues
}
return sb.NotIn(fieldName, values), nil
case qbtypes.FilterOperatorExists, qbtypes.FilterOperatorNotExists:
intrinsic := []string{"rule_id", "rule_name", "overall_state", "overall_state_changed", "state", "state_changed", "unix_milli", "fingerprint", "value"}
if slices.Contains(intrinsic, key.Name) {
return "true", nil
}
if operator == qbtypes.FilterOperatorExists {
return fmt.Sprintf("has(JSONExtractKeys(labels), %s)", sb.Var(key.Name)), nil
}
return fmt.Sprintf("not has(JSONExtractKeys(labels), %s)", sb.Var(key.Name)), nil
}
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported operator: %v", operator)
}

View File

@@ -0,0 +1,62 @@
package implrulestatehistory
import (
"context"
"fmt"
"strings"
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/huandu/go-sqlbuilder"
)
var ruleStateHistoryColumns = map[string]*schema.Column{
"rule_id": {Name: "rule_id", Type: schema.ColumnTypeString},
"rule_name": {Name: "rule_name", Type: schema.ColumnTypeString},
"overall_state": {Name: "overall_state", Type: schema.ColumnTypeString},
"overall_state_changed": {Name: "overall_state_changed", Type: schema.ColumnTypeBool},
"state": {Name: "state", Type: schema.ColumnTypeString},
"state_changed": {Name: "state_changed", Type: schema.ColumnTypeBool},
"unix_milli": {Name: "unix_milli", Type: schema.ColumnTypeInt64},
"labels": {Name: "labels", Type: schema.ColumnTypeString},
"fingerprint": {Name: "fingerprint", Type: schema.ColumnTypeUInt64},
"value": {Name: "value", Type: schema.ColumnTypeFloat64},
}
type fieldMapper struct{}
func newFieldMapper() qbtypes.FieldMapper {
return &fieldMapper{}
}
func (m *fieldMapper) getColumn(_ context.Context, key *telemetrytypes.TelemetryFieldKey) (*schema.Column, error) {
name := strings.TrimSpace(key.Name)
if col, ok := ruleStateHistoryColumns[name]; ok {
return col, nil
}
return ruleStateHistoryColumns["labels"], nil
}
func (m *fieldMapper) FieldFor(ctx context.Context, key *telemetrytypes.TelemetryFieldKey) (string, error) {
col, err := m.getColumn(ctx, key)
if err != nil {
return "", err
}
if col.Name == "labels" && key.Name != "labels" {
return fmt.Sprintf("JSONExtractString(labels, '%s')", strings.ReplaceAll(key.Name, "'", "\\'")), nil
}
return col.Name, nil
}
func (m *fieldMapper) ColumnFor(ctx context.Context, key *telemetrytypes.TelemetryFieldKey) (*schema.Column, error) {
return m.getColumn(ctx, key)
}
func (m *fieldMapper) ColumnExpressionFor(ctx context.Context, field *telemetrytypes.TelemetryFieldKey, _ map[string][]*telemetrytypes.TelemetryFieldKey) (string, error) {
colName, err := m.FieldFor(ctx, field)
if err != nil {
return "", err
}
return fmt.Sprintf("%s AS `%s`", sqlbuilder.Escape(colName), field.Name), nil
}

View File

@@ -0,0 +1,351 @@
package implrulestatehistory
import (
"encoding/base64"
"encoding/json"
"net/http"
"sort"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/gorilla/mux"
)
type handler struct {
module rulestatehistory.Module
}
type ruleHistoryRequest struct {
Query rulestatehistorytypes.Query
Cursor string
}
type cursorToken struct {
Offset int64 `json:"offset"`
Limit int64 `json:"limit"`
}
func NewHandler(module rulestatehistory.Module) rulestatehistory.Handler {
return &handler{module: module}
}
func (h *handler) GetRuleHistoryStats(w http.ResponseWriter, r *http.Request) {
ruleID := mux.Vars(r)["id"]
req, ok := h.parseV2BaseQueryRequest(w, r)
if !ok {
return
}
stats, err := h.module.GetHistoryStats(r.Context(), ruleID, req.Query)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, stats)
}
func (h *handler) GetRuleHistoryOverallStatus(w http.ResponseWriter, r *http.Request) {
ruleID := mux.Vars(r)["id"]
req, ok := h.parseV2BaseQueryRequest(w, r)
if !ok {
return
}
res, err := h.module.GetHistoryOverallStatus(r.Context(), ruleID, req.Query)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, res)
}
func (h *handler) GetRuleHistoryTimeline(w http.ResponseWriter, r *http.Request) {
ruleID := mux.Vars(r)["id"]
req, ok := h.parseV2TimelineQueryRequest(w, r)
if !ok {
return
}
if req.Cursor != "" {
token, err := decodeCursor(req.Cursor)
if err != nil {
render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid cursor"))
return
}
req.Query.Offset = token.Offset
if req.Query.Limit == 0 {
req.Query.Limit = token.Limit
}
}
if req.Query.Limit == 0 {
req.Query.Limit = 50
}
timelineItems, timelineTotal, err := h.module.GetHistoryTimeline(r.Context(), ruleID, req.Query)
if err != nil {
render.Error(w, err)
return
}
resp := rulestatehistorytypes.RuleStateTimelineResponse{}
resp.Items = make([]rulestatehistorytypes.RuleStateHistoryResponseItem, 0, len(timelineItems))
for _, item := range timelineItems {
resp.Items = append(resp.Items, rulestatehistorytypes.RuleStateHistoryResponseItem{
RuleID: item.RuleID,
RuleName: item.RuleName,
OverallState: item.OverallState,
OverallStateChanged: item.OverallStateChanged,
State: item.State,
StateChanged: item.StateChanged,
UnixMilli: item.UnixMilli,
Labels: toQBLabels(item.Labels),
Fingerprint: item.Fingerprint,
Value: item.Value,
})
}
resp.Total = timelineTotal
if req.Query.Limit > 0 && req.Query.Offset+int64(len(timelineItems)) < int64(timelineTotal) {
nextOffset := req.Query.Offset + int64(len(timelineItems))
nextCursor, err := encodeCursor(cursorToken{Offset: nextOffset, Limit: req.Query.Limit})
if err != nil {
render.Error(w, err)
return
}
resp.NextCursor = nextCursor
}
render.Success(w, http.StatusOK, resp)
}
func (h *handler) GetRuleHistoryContributors(w http.ResponseWriter, r *http.Request) {
ruleID := mux.Vars(r)["id"]
req, ok := h.parseV2BaseQueryRequest(w, r)
if !ok {
return
}
res, err := h.module.GetHistoryContributors(r.Context(), ruleID, req.Query)
if err != nil {
render.Error(w, err)
return
}
converted := make([]rulestatehistorytypes.RuleStateHistoryContributorResponse, 0, len(res))
for _, item := range res {
converted = append(converted, rulestatehistorytypes.RuleStateHistoryContributorResponse{
Fingerprint: item.Fingerprint,
Labels: toQBLabels(item.Labels),
Count: item.Count,
RelatedTracesLink: item.RelatedTracesLink,
RelatedLogsLink: item.RelatedLogsLink,
})
}
render.Success(w, http.StatusOK, converted)
}
func (h *handler) GetRuleHistoryFilterKeys(w http.ResponseWriter, r *http.Request) {
ruleID := mux.Vars(r)["id"]
query, search, limit, ok := h.parseV2FilterKeysRequest(w, r)
if !ok {
return
}
res, err := h.module.GetHistoryFilterKeys(r.Context(), ruleID, query, search, limit)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, res)
}
func (h *handler) GetRuleHistoryFilterValues(w http.ResponseWriter, r *http.Request) {
ruleID := mux.Vars(r)["id"]
query, key, search, limit, ok := h.parseV2FilterValuesRequest(w, r)
if !ok {
return
}
res, err := h.module.GetHistoryFilterValues(r.Context(), ruleID, key, query, search, limit)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, res)
}
func (h *handler) parseV2BaseQueryRequest(w http.ResponseWriter, r *http.Request) (*ruleHistoryRequest, bool) {
req, err := parseV2BaseQueryFromURL(r)
if err != nil {
render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query parameters"))
return nil, false
}
if req.Query.Start == 0 || req.Query.End == 0 || req.Query.Start >= req.Query.End {
render.Error(w, errors.NewInvalidInputf(errors.CodeInvalidInput, "start and end are required and start must be less than end"))
return nil, false
}
return req, true
}
func (h *handler) parseV2TimelineQueryRequest(w http.ResponseWriter, r *http.Request) (*ruleHistoryRequest, bool) {
req, err := parseV2TimelineQueryFromURL(r)
if err != nil {
render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query parameters"))
return nil, false
}
if err := req.Query.Validate(); err != nil {
render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query parameters"))
return nil, false
}
return req, true
}
func (h *handler) parseV2FilterKeysRequest(w http.ResponseWriter, r *http.Request) (rulestatehistorytypes.Query, string, int64, bool) {
raw := telemetrytypes.PostableFieldKeysParams{}
if err := binding.Query.BindQuery(r.URL.Query(), &raw); err != nil {
render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query parameters"))
return rulestatehistorytypes.Query{}, "", 0, false
}
query := rulestatehistorytypes.Query{
Start: raw.StartUnixMilli,
End: raw.EndUnixMilli,
FilterExpression: qbtypes.Filter{},
Order: qbtypes.OrderDirectionAsc,
}
if err := query.Validate(); err != nil {
render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query parameters"))
return rulestatehistorytypes.Query{}, "", 0, false
}
limit := normalizeFilterLimit(int64(raw.Limit))
return query, strings.TrimSpace(raw.SearchText), limit, true
}
func (h *handler) parseV2FilterValuesRequest(w http.ResponseWriter, r *http.Request) (rulestatehistorytypes.Query, string, string, int64, bool) {
raw := telemetrytypes.PostableFieldValueParams{}
if err := binding.Query.BindQuery(r.URL.Query(), &raw); err != nil {
render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query parameters"))
return rulestatehistorytypes.Query{}, "", "", 0, false
}
key := strings.TrimSpace(raw.Name)
if key == "" {
render.Error(w, errors.NewInvalidInputf(errors.CodeInvalidInput, "key is required"))
return rulestatehistorytypes.Query{}, "", "", 0, false
}
query := rulestatehistorytypes.Query{
Start: raw.StartUnixMilli,
End: raw.EndUnixMilli,
FilterExpression: parseFilterExpression(raw.ExistingQuery),
Order: qbtypes.OrderDirectionAsc,
}
if err := query.Validate(); err != nil {
render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query parameters"))
return rulestatehistorytypes.Query{}, "", "", 0, false
}
limit := normalizeFilterLimit(int64(raw.Limit))
return query, key, strings.TrimSpace(raw.SearchText), limit, true
}
func parseV2BaseQueryFromURL(r *http.Request) (*ruleHistoryRequest, error) {
raw := rulestatehistorytypes.V2HistoryBaseQueryParams{}
if err := binding.Query.BindQuery(r.URL.Query(), &raw); err != nil {
return nil, err
}
req := &ruleHistoryRequest{}
req.Query.Start = raw.Start
req.Query.End = raw.End
return req, nil
}
func parseV2TimelineQueryFromURL(r *http.Request) (*ruleHistoryRequest, error) {
raw := rulestatehistorytypes.V2HistoryTimelineQueryParams{}
if err := binding.Query.BindQuery(r.URL.Query(), &raw); err != nil {
return nil, err
}
req := &ruleHistoryRequest{}
req.Query.Start = raw.Start
req.Query.End = raw.End
req.Query.State = raw.State
req.Query.Limit = raw.Limit
req.Query.Order = raw.Order
req.Query.FilterExpression = parseFilterExpression(raw.FilterExpression)
req.Cursor = raw.Cursor
return req, nil
}
func encodeCursor(token cursorToken) (string, error) {
data, err := json.Marshal(token)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(data), nil
}
func decodeCursor(cursor string) (*cursorToken, error) {
data, err := base64.RawURLEncoding.DecodeString(cursor)
if err != nil {
return nil, err
}
token := &cursorToken{}
if err := json.Unmarshal(data, token); err != nil {
return nil, err
}
return token, nil
}
func normalizeFilterLimit(limit int64) int64 {
if limit <= 0 {
return 50
}
if limit > 200 {
return 200
}
return limit
}
func toQBLabels(raw rulestatehistorytypes.LabelsString) []*qbtypes.Label {
if strings.TrimSpace(string(raw)) == "" {
return []*qbtypes.Label{}
}
labelsMap := map[string]any{}
if err := json.Unmarshal([]byte(raw), &labelsMap); err != nil {
return []*qbtypes.Label{}
}
keys := make([]string, 0, len(labelsMap))
for key := range labelsMap {
keys = append(keys, key)
}
sort.Strings(keys)
labels := make([]*qbtypes.Label, 0, len(keys))
for _, key := range keys {
labels = append(labels, &qbtypes.Label{
Key: telemetrytypes.TelemetryFieldKey{
Name: key,
},
Value: labelsMap[key],
})
}
return labels
}
func parseFilterExpression(values ...string) qbtypes.Filter {
for _, value := range values {
expr := strings.TrimSpace(value)
if expr != "" {
return qbtypes.Filter{Expression: expr}
}
}
return qbtypes.Filter{}
}

View File

@@ -0,0 +1,183 @@
package implrulestatehistory
import (
"context"
"math"
"time"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
type module struct {
store rulestatehistorytypes.Store
}
func NewModule(store rulestatehistorytypes.Store) rulestatehistory.Module {
return &module{store: store}
}
func (m *module) AddRuleStateHistory(ctx context.Context, entries []rulestatehistorytypes.RuleStateHistory) error {
return m.store.AddRuleStateHistory(ctx, entries)
}
func (m *module) GetLastSavedRuleStateHistory(ctx context.Context, ruleID string) ([]rulestatehistorytypes.RuleStateHistory, error) {
return m.store.GetLastSavedRuleStateHistory(ctx, ruleID)
}
func (m *module) GetHistoryTimeline(ctx context.Context, ruleID string, query rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistory, uint64, error) {
return m.store.ReadRuleStateHistoryByRuleID(ctx, ruleID, &query)
}
func (m *module) GetHistoryFilterKeys(ctx context.Context, ruleID string, query rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldKeys, error) {
return m.store.ReadRuleStateHistoryFilterKeysByRuleID(ctx, ruleID, &query, search, limit)
}
func (m *module) GetHistoryFilterValues(ctx context.Context, ruleID string, key string, query rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldValues, error) {
return m.store.ReadRuleStateHistoryFilterValuesByRuleID(ctx, ruleID, key, &query, search, limit)
}
func (m *module) GetHistoryContributors(ctx context.Context, ruleID string, query rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistoryContributor, error) {
return m.store.ReadRuleStateHistoryTopContributorsByRuleID(ctx, ruleID, &query)
}
func (m *module) GetHistoryOverallStatus(ctx context.Context, ruleID string, query rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateWindow, error) {
return m.store.GetOverallStateTransitions(ctx, ruleID, &query)
}
func (m *module) GetHistoryStats(ctx context.Context, ruleID string, params rulestatehistorytypes.Query) (rulestatehistorytypes.Stats, error) {
totalCurrentTriggers, err := m.store.GetTotalTriggers(ctx, ruleID, &params)
if err != nil {
return rulestatehistorytypes.Stats{}, err
}
currentTriggersSeries, err := m.store.GetTriggersByInterval(ctx, ruleID, &params)
if err != nil {
return rulestatehistorytypes.Stats{}, err
}
currentAvgResolutionTime, err := m.store.GetAvgResolutionTime(ctx, ruleID, &params)
if err != nil {
return rulestatehistorytypes.Stats{}, err
}
currentAvgResolutionTimeSeries, err := m.store.GetAvgResolutionTimeByInterval(ctx, ruleID, &params)
if err != nil {
return rulestatehistorytypes.Stats{}, err
}
if params.End-params.Start >= 86400000 {
days := int64(math.Ceil(float64(params.End-params.Start) / 86400000))
params.Start -= days * 86400000
params.End -= days * 86400000
} else {
params.Start -= 86400000
params.End -= 86400000
}
totalPastTriggers, err := m.store.GetTotalTriggers(ctx, ruleID, &params)
if err != nil {
return rulestatehistorytypes.Stats{}, err
}
pastTriggersSeries, err := m.store.GetTriggersByInterval(ctx, ruleID, &params)
if err != nil {
return rulestatehistorytypes.Stats{}, err
}
pastAvgResolutionTime, err := m.store.GetAvgResolutionTime(ctx, ruleID, &params)
if err != nil {
return rulestatehistorytypes.Stats{}, err
}
pastAvgResolutionTimeSeries, err := m.store.GetAvgResolutionTimeByInterval(ctx, ruleID, &params)
if err != nil {
return rulestatehistorytypes.Stats{}, err
}
if math.IsNaN(currentAvgResolutionTime) || math.IsInf(currentAvgResolutionTime, 0) {
currentAvgResolutionTime = 0
}
if math.IsNaN(pastAvgResolutionTime) || math.IsInf(pastAvgResolutionTime, 0) {
pastAvgResolutionTime = 0
}
return rulestatehistorytypes.Stats{
TotalCurrentTriggers: totalCurrentTriggers,
TotalPastTriggers: totalPastTriggers,
CurrentTriggersSeries: currentTriggersSeries,
PastTriggersSeries: pastTriggersSeries,
CurrentAvgResolutionTime: currentAvgResolutionTime,
PastAvgResolutionTime: pastAvgResolutionTime,
CurrentAvgResolutionTimeSeries: currentAvgResolutionTimeSeries,
PastAvgResolutionTimeSeries: pastAvgResolutionTimeSeries,
}, nil
}
func (m *module) RecordRuleStateHistory(ctx context.Context, ruleID string, handledRestart bool, itemsToAdd []rulestatehistorytypes.RuleStateHistory) error {
revisedItemsToAdd := map[uint64]rulestatehistorytypes.RuleStateHistory{}
lastSavedState, err := m.store.GetLastSavedRuleStateHistory(ctx, ruleID)
if err != nil {
return err
}
if !handledRestart && len(lastSavedState) > 0 {
currentItemsByFingerprint := make(map[uint64]rulestatehistorytypes.RuleStateHistory, len(itemsToAdd))
for _, item := range itemsToAdd {
currentItemsByFingerprint[item.Fingerprint] = item
}
shouldSkip := map[uint64]bool{}
for _, item := range lastSavedState {
currentState, ok := currentItemsByFingerprint[item.Fingerprint]
if !ok {
if item.State == rulestatehistorytypes.StateFiring || item.State == rulestatehistorytypes.StateNoData {
item.State = rulestatehistorytypes.StateInactive
item.StateChanged = true
item.UnixMilli = time.Now().UnixMilli()
revisedItemsToAdd[item.Fingerprint] = item
}
} else if item.State != currentState.State {
item.State = currentState.State
item.StateChanged = true
item.UnixMilli = time.Now().UnixMilli()
revisedItemsToAdd[item.Fingerprint] = item
}
shouldSkip[item.Fingerprint] = true
}
for _, item := range itemsToAdd {
if _, ok := revisedItemsToAdd[item.Fingerprint]; !ok && !shouldSkip[item.Fingerprint] {
revisedItemsToAdd[item.Fingerprint] = item
}
}
newState := rulestatehistorytypes.StateInactive
for _, item := range revisedItemsToAdd {
if item.State == rulestatehistorytypes.StateFiring || item.State == rulestatehistorytypes.StateNoData {
newState = rulestatehistorytypes.StateFiring
break
}
}
if lastSavedState[0].OverallState != newState {
for fingerprint, item := range revisedItemsToAdd {
item.OverallState = newState
item.OverallStateChanged = true
revisedItemsToAdd[fingerprint] = item
}
}
} else {
for _, item := range itemsToAdd {
revisedItemsToAdd[item.Fingerprint] = item
}
}
if len(revisedItemsToAdd) == 0 {
return nil
}
entries := make([]rulestatehistorytypes.RuleStateHistory, 0, len(revisedItemsToAdd))
for _, item := range revisedItemsToAdd {
entries = append(entries, item)
}
return m.store.AddRuleStateHistory(ctx, entries)
}

View File

@@ -0,0 +1,574 @@
package implrulestatehistory
import (
"context"
"fmt"
"log/slog"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetrystore"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
sqlbuilder "github.com/huandu/go-sqlbuilder"
)
const (
signozHistoryDBName = "signoz_analytics"
ruleStateHistoryTableName = "distributed_rule_state_history_v0"
)
type store struct {
telemetryStore telemetrystore.TelemetryStore
telemetryMetadataStore telemetrytypes.MetadataStore
fieldMapper qbtypes.FieldMapper
conditionBuilder qbtypes.ConditionBuilder
logger *slog.Logger
}
func NewStore(telemetryStore telemetrystore.TelemetryStore, telemetryMetadataStore telemetrytypes.MetadataStore, logger *slog.Logger) rulestatehistorytypes.Store {
fm := newFieldMapper()
return &store{
telemetryStore: telemetryStore,
telemetryMetadataStore: telemetryMetadataStore,
fieldMapper: fm,
conditionBuilder: newConditionBuilder(fm),
logger: logger,
}
}
func (s *store) AddRuleStateHistory(ctx context.Context, entries []rulestatehistorytypes.RuleStateHistory) error {
ib := sqlbuilder.NewInsertBuilder()
ib.InsertInto(historyTable())
ib.Cols(
"rule_id",
"rule_name",
"overall_state",
"overall_state_changed",
"state",
"state_changed",
"unix_milli",
"labels",
"fingerprint",
"value",
)
insertQuery, _ := ib.BuildWithFlavor(sqlbuilder.ClickHouse)
statement, err := s.telemetryStore.ClickhouseDB().PrepareBatch(
ctx,
insertQuery,
)
if err != nil {
return err
}
defer statement.Abort() //nolint:errcheck
for _, history := range entries {
if err = statement.Append(
history.RuleID,
history.RuleName,
history.OverallState,
history.OverallStateChanged,
history.State,
history.StateChanged,
history.UnixMilli,
history.Labels,
history.Fingerprint,
history.Value,
); err != nil {
return err
}
}
return statement.Send()
}
func (s *store) GetLastSavedRuleStateHistory(ctx context.Context, ruleID string) ([]rulestatehistorytypes.RuleStateHistory, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select("*")
sb.From(historyTable())
sb.Where(sb.E("rule_id", ruleID))
sb.Where(sb.E("state_changed", true))
sb.OrderBy("unix_milli DESC")
sb.SQL("LIMIT 1 BY fingerprint")
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
history := make([]rulestatehistorytypes.RuleStateHistory, 0)
if err := s.telemetryStore.ClickhouseDB().Select(ctx, &history, query, args...); err != nil {
return nil, err
}
return history, nil
}
func (s *store) ReadRuleStateHistoryByRuleID(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistory, uint64, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select(
"rule_id",
"rule_name",
"overall_state",
"overall_state_changed",
"state",
"state_changed",
"unix_milli",
"labels",
"fingerprint",
"value",
)
sb.From(historyTable())
s.applyBaseHistoryFilters(sb, ruleID, query)
whereClause, err := s.buildFilterClause(ctx, query.FilterExpression, query.Start, query.End)
if err != nil {
return nil, 0, err
}
if whereClause != nil {
sb.AddWhereClause(sqlbuilder.CopyWhereClause(whereClause))
}
sb.OrderBy(fmt.Sprintf("unix_milli %s", strings.ToUpper(query.Order.StringValue())))
sb.Limit(int(query.Limit))
sb.Offset(int(query.Offset))
selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
history := []rulestatehistorytypes.RuleStateHistory{}
if err := s.telemetryStore.ClickhouseDB().Select(ctx, &history, selectQuery, args...); err != nil {
return nil, 0, err
}
countSB := sqlbuilder.NewSelectBuilder()
countSB.Select("count(*)")
countSB.From(historyTable())
s.applyBaseHistoryFilters(countSB, ruleID, query)
if whereClause != nil {
countSB.AddWhereClause(sqlbuilder.CopyWhereClause(whereClause))
}
var total uint64
countQuery, countArgs := countSB.BuildWithFlavor(sqlbuilder.ClickHouse)
if err := s.telemetryStore.ClickhouseDB().QueryRow(ctx, countQuery, countArgs...).Scan(&total); err != nil {
return nil, 0, err
}
return history, total, nil
}
func (s *store) ReadRuleStateHistoryFilterKeysByRuleID(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldKeys, error) {
if limit <= 0 {
limit = 50
}
sb := sqlbuilder.NewSelectBuilder()
keyExpr := "arrayJoin(JSONExtractKeys(labels))"
sb.Select(fmt.Sprintf("DISTINCT %s AS key", keyExpr))
sb.From(historyTable())
s.applyBaseHistoryFilters(sb, ruleID, query)
sb.Where(fmt.Sprintf("%s != ''", keyExpr))
search = strings.TrimSpace(search)
if search != "" {
sb.Where(fmt.Sprintf("positionCaseInsensitiveUTF8(%s, %s) > 0", keyExpr, sb.Var(search)))
}
whereClause, err := s.buildFilterClause(ctx, query.FilterExpression, query.Start, query.End)
if err != nil {
return nil, err
}
if whereClause != nil {
sb.AddWhereClause(sqlbuilder.CopyWhereClause(whereClause))
}
sb.OrderBy("key ASC")
sb.Limit(int(limit + 1))
selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := s.telemetryStore.ClickhouseDB().Query(ctx, selectQuery, args...)
if err != nil {
return nil, err
}
defer rows.Close()
keys := make([]string, 0, limit+1)
for rows.Next() {
var key string
if err := rows.Scan(&key); err != nil {
return nil, err
}
key = strings.TrimSpace(key)
if key != "" {
keys = append(keys, key)
}
}
if err := rows.Err(); err != nil {
return nil, err
}
complete := true
if int64(len(keys)) > limit {
keys = keys[:int(limit)]
complete = false
}
keysMap := make(map[string][]*telemetrytypes.TelemetryFieldKey, len(keys))
for _, key := range keys {
fieldKey := &telemetrytypes.TelemetryFieldKey{
Name: key,
FieldDataType: telemetrytypes.FieldDataTypeString,
}
keysMap[key] = []*telemetrytypes.TelemetryFieldKey{fieldKey}
}
return &telemetrytypes.GettableFieldKeys{
Keys: keysMap,
Complete: complete,
}, nil
}
func (s *store) ReadRuleStateHistoryFilterValuesByRuleID(ctx context.Context, ruleID string, key string, query *rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldValues, error) {
if limit <= 0 {
limit = 50
}
sb := sqlbuilder.NewSelectBuilder()
valExpr := fmt.Sprintf("JSONExtractString(labels, %s)", sb.Var(key))
sb.Select(fmt.Sprintf("DISTINCT %s AS val", valExpr))
sb.From(historyTable())
s.applyBaseHistoryFilters(sb, ruleID, query)
sb.Where(fmt.Sprintf("JSONHas(labels, %s)", sb.Var(key)))
sb.Where(fmt.Sprintf("%s != ''", valExpr))
search = strings.TrimSpace(search)
if search != "" {
sb.Where(fmt.Sprintf("positionCaseInsensitiveUTF8(%s, %s) > 0", valExpr, sb.Var(search)))
}
whereClause, err := s.buildFilterClause(ctx, query.FilterExpression, query.Start, query.End)
if err != nil {
return nil, err
}
if whereClause != nil {
sb.AddWhereClause(sqlbuilder.CopyWhereClause(whereClause))
}
sb.OrderBy("val ASC")
sb.Limit(int(limit + 1))
selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := s.telemetryStore.ClickhouseDB().Query(ctx, selectQuery, args...)
if err != nil {
return nil, err
}
defer rows.Close()
values := make([]string, 0, limit+1)
for rows.Next() {
var value string
if err := rows.Scan(&value); err != nil {
return nil, err
}
value = strings.TrimSpace(value)
if value != "" {
values = append(values, value)
}
}
if err := rows.Err(); err != nil {
return nil, err
}
complete := true
if int64(len(values)) > limit {
values = values[:int(limit)]
complete = false
}
return &telemetrytypes.GettableFieldValues{
Values: &telemetrytypes.TelemetryFieldValues{
StringValues: values,
},
Complete: complete,
}, nil
}
func (s *store) ReadRuleStateHistoryTopContributorsByRuleID(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistoryContributor, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select(
"fingerprint",
"argMax(labels, unix_milli) AS labels",
"count(*) AS count",
)
sb.From(historyTable())
sb.Where(sb.E("rule_id", ruleID))
sb.Where(sb.E("state_changed", true))
sb.Where(sb.E("state", rulestatehistorytypes.StateFiring.StringValue()))
sb.Where(sb.GE("unix_milli", query.Start))
sb.Where(sb.LT("unix_milli", query.End))
whereClause, err := s.buildFilterClause(ctx, query.FilterExpression, query.Start, query.End)
if err != nil {
return nil, err
}
if whereClause != nil {
sb.AddWhereClause(sqlbuilder.CopyWhereClause(whereClause))
}
sb.GroupBy("fingerprint")
sb.Having("labels != '{}'")
sb.OrderBy("count DESC")
selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
contributors := []rulestatehistorytypes.RuleStateHistoryContributor{}
if err := s.telemetryStore.ClickhouseDB().Select(ctx, &contributors, selectQuery, args...); err != nil {
return nil, err
}
return contributors, nil
}
func (s *store) GetOverallStateTransitions(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateWindow, error) {
innerSB := sqlbuilder.NewSelectBuilder()
eventsSubquery := fmt.Sprintf(
`SELECT %s AS ts, if(count(*) = 0, %s, argMax(overall_state, unix_milli)) AS state
FROM %s
WHERE rule_id = %s
AND unix_milli <= %s
UNION ALL
SELECT unix_milli AS ts, anyLast(overall_state) AS state
FROM %s
WHERE rule_id = %s
AND overall_state_changed = true
AND unix_milli > %s
AND unix_milli < %s
GROUP BY unix_milli`,
innerSB.Var(query.Start),
innerSB.Var(rulestatehistorytypes.StateInactive.StringValue()),
historyTable(),
innerSB.Var(ruleID),
innerSB.Var(query.Start),
historyTable(),
innerSB.Var(ruleID),
innerSB.Var(query.Start),
innerSB.Var(query.End),
)
innerSB.Select(
"state",
"ts AS start",
fmt.Sprintf(
"ifNull(leadInFrame(toNullable(ts), 1) OVER (ORDER BY ts ASC ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING), %s) AS end",
innerSB.Var(query.End),
),
)
innerSB.From(fmt.Sprintf("(%s) AS events", eventsSubquery))
innerSB.OrderBy("start ASC")
innerQuery, args := innerSB.BuildWithFlavor(sqlbuilder.ClickHouse)
outerSB := sqlbuilder.NewSelectBuilder()
outerSB.Select("state", "start", "end")
outerSB.From(fmt.Sprintf("(%s) AS windows", innerQuery))
outerSB.Where("start < end")
selectQuery, outerArgs := outerSB.BuildWithFlavor(sqlbuilder.ClickHouse)
args = append(args, outerArgs...)
windows := []rulestatehistorytypes.RuleStateWindow{}
if err := s.telemetryStore.ClickhouseDB().Select(ctx, &windows, selectQuery, args...); err != nil {
return nil, err
}
return windows, nil
}
func (s *store) GetAvgResolutionTime(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) (float64, error) {
cte := s.buildMatchedEventsCTE(ruleID, query)
sb := cte.Select("ifNull(toFloat64(avg(resolution_time - firing_time)) / 1000, 0) AS avg_resolution_time")
sb.From("matched_events")
selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
var avg float64
if err := s.telemetryStore.ClickhouseDB().QueryRow(ctx, selectQuery, args...).Scan(&avg); err != nil {
return 0, err
}
return avg, nil
}
func (s *store) GetAvgResolutionTimeByInterval(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) (*qbtypes.TimeSeries, error) {
step := minStepSeconds(query.Start, query.End)
cte := s.buildMatchedEventsCTE(ruleID, query)
sb := cte.Select(
fmt.Sprintf("toFloat64(avg(resolution_time - firing_time)) / 1000 AS value, toStartOfInterval(toDateTime(intDiv(firing_time, 1000)), INTERVAL %d SECOND) AS ts", step),
)
sb.From("matched_events")
sb.GroupBy("ts")
sb.OrderBy("ts ASC")
selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return s.querySeries(ctx, selectQuery, args...)
}
func (s *store) GetTotalTriggers(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) (uint64, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select("count(*)")
sb.From(historyTable())
sb.Where(sb.E("rule_id", ruleID))
sb.Where(sb.E("state_changed", true))
sb.Where(sb.E("state", rulestatehistorytypes.StateFiring.StringValue()))
sb.Where(sb.GE("unix_milli", query.Start))
sb.Where(sb.LT("unix_milli", query.End))
selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
var total uint64
if err := s.telemetryStore.ClickhouseDB().QueryRow(ctx, selectQuery, args...).Scan(&total); err != nil {
return 0, err
}
return total, nil
}
func (s *store) GetTriggersByInterval(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) (*qbtypes.TimeSeries, error) {
step := minStepSeconds(query.Start, query.End)
sb := sqlbuilder.NewSelectBuilder()
sb.Select(
fmt.Sprintf("toFloat64(count(*)) AS value, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL %d SECOND) AS ts", step),
)
sb.From(historyTable())
sb.Where(sb.E("rule_id", ruleID))
sb.Where(sb.E("state_changed", true))
sb.Where(sb.E("state", rulestatehistorytypes.StateFiring.StringValue()))
sb.Where(sb.GE("unix_milli", query.Start))
sb.Where(sb.LT("unix_milli", query.End))
sb.GroupBy("ts")
sb.OrderBy("ts ASC")
selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return s.querySeries(ctx, selectQuery, args...)
}
func (s *store) querySeries(ctx context.Context, selectQuery string, args ...any) (*qbtypes.TimeSeries, error) {
rows, err := s.telemetryStore.ClickhouseDB().Query(ctx, selectQuery, args...)
if err != nil {
return nil, err
}
defer rows.Close()
series := &qbtypes.TimeSeries{
Labels: []*qbtypes.Label{},
Values: []*qbtypes.TimeSeriesValue{},
}
for rows.Next() {
var value float64
var ts time.Time
if err := rows.Scan(&value, &ts); err != nil {
return nil, err
}
series.Values = append(series.Values, &qbtypes.TimeSeriesValue{
Timestamp: ts.UnixMilli(),
Value: value,
})
}
if err := rows.Err(); err != nil {
return nil, err
}
return series, nil
}
func (s *store) buildFilterClause(ctx context.Context, filter qbtypes.Filter, startMillis, endMillis int64) (*sqlbuilder.WhereClause, error) {
expression := strings.TrimSpace(filter.Expression)
if expression == "" {
return nil, nil //nolint:nilnil
}
selectors := querybuilder.QueryStringToKeysSelectors(expression)
for i := range selectors {
selectors[i].SelectorMatchType = telemetrytypes.FieldSelectorMatchTypeExact
}
fieldKeys, _, err := s.telemetryMetadataStore.GetKeysMulti(ctx, selectors)
if err != nil || len(fieldKeys) == 0 {
fieldKeys = map[string][]*telemetrytypes.TelemetryFieldKey{}
for _, sel := range selectors {
fieldKeys[sel.Name] = []*telemetrytypes.TelemetryFieldKey{{
Name: sel.Name,
Signal: sel.Signal,
FieldContext: sel.FieldContext,
FieldDataType: sel.FieldDataType,
}}
}
}
opts := querybuilder.FilterExprVisitorOpts{
Logger: s.logger,
FieldMapper: s.fieldMapper,
ConditionBuilder: s.conditionBuilder,
FieldKeys: fieldKeys,
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels", FieldContext: telemetrytypes.FieldContextAttribute},
}
startNs := querybuilder.ToNanoSecs(uint64(startMillis))
endNs := querybuilder.ToNanoSecs(uint64(endMillis))
prepared, err := querybuilder.PrepareWhereClause(expression, opts, startNs, endNs)
if err != nil {
return nil, err
}
if prepared == nil || prepared.WhereClause == nil {
return nil, nil //nolint:nilnil
}
return prepared.WhereClause, nil
}
func (s *store) applyBaseHistoryFilters(sb *sqlbuilder.SelectBuilder, ruleID string, query *rulestatehistorytypes.Query) {
sb.Where(sb.E("rule_id", ruleID))
sb.Where(sb.GE("unix_milli", query.Start))
sb.Where(sb.LT("unix_milli", query.End))
if !query.State.IsZero() {
sb.Where(sb.E("state", query.State.StringValue()))
}
}
func (s *store) buildMatchedEventsCTE(ruleID string, query *rulestatehistorytypes.Query) *sqlbuilder.CTEBuilder {
firingSB := sqlbuilder.NewSelectBuilder()
firingSB.Select("rule_id", "unix_milli AS firing_time")
firingSB.From(historyTable())
firingSB.Where(firingSB.E("overall_state", rulestatehistorytypes.StateFiring.StringValue()))
firingSB.Where(firingSB.E("overall_state_changed", true))
firingSB.Where(firingSB.E("rule_id", ruleID))
firingSB.Where(firingSB.GE("unix_milli", query.Start))
firingSB.Where(firingSB.LT("unix_milli", query.End))
resolutionSB := sqlbuilder.NewSelectBuilder()
resolutionSB.Select("rule_id", "unix_milli AS resolution_time")
resolutionSB.From(historyTable())
resolutionSB.Where(resolutionSB.E("overall_state", rulestatehistorytypes.StateInactive.StringValue()))
resolutionSB.Where(resolutionSB.E("overall_state_changed", true))
resolutionSB.Where(resolutionSB.E("rule_id", ruleID))
resolutionSB.Where(resolutionSB.GE("unix_milli", query.Start))
resolutionSB.Where(resolutionSB.LT("unix_milli", query.End))
matchedSB := sqlbuilder.NewSelectBuilder()
matchedSB.Select("f.rule_id", "f.firing_time", "min(r.resolution_time) AS resolution_time")
matchedSB.From("firing_events f")
matchedSB.JoinWithOption(sqlbuilder.LeftJoin, "resolution_events r", "f.rule_id = r.rule_id")
matchedSB.Where("r.resolution_time > f.firing_time")
matchedSB.GroupBy("f.rule_id", "f.firing_time")
return sqlbuilder.With(
sqlbuilder.CTEQuery("firing_events").As(firingSB),
sqlbuilder.CTEQuery("resolution_events").As(resolutionSB),
sqlbuilder.CTEQuery("matched_events").As(matchedSB),
)
}
func historyTable() string {
return fmt.Sprintf("%s.%s", signozHistoryDBName, ruleStateHistoryTableName)
}
func minStepSeconds(start, end int64) int64 {
if end <= start {
return 60
}
rangeSeconds := (end - start) / 1000
if rangeSeconds <= 0 {
return 60
}
step := rangeSeconds / 120
return max(step, int64(60))
}

View File

@@ -0,0 +1,30 @@
package rulestatehistory
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
type Module interface {
RecordRuleStateHistory(context.Context, string, bool, []rulestatehistorytypes.RuleStateHistory) error
AddRuleStateHistory(context.Context, []rulestatehistorytypes.RuleStateHistory) error
GetLastSavedRuleStateHistory(context.Context, string) ([]rulestatehistorytypes.RuleStateHistory, error)
GetHistoryStats(context.Context, string, rulestatehistorytypes.Query) (rulestatehistorytypes.Stats, error)
GetHistoryTimeline(context.Context, string, rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistory, uint64, error)
GetHistoryFilterKeys(context.Context, string, rulestatehistorytypes.Query, string, int64) (*telemetrytypes.GettableFieldKeys, error)
GetHistoryFilterValues(context.Context, string, string, rulestatehistorytypes.Query, string, int64) (*telemetrytypes.GettableFieldValues, error)
GetHistoryContributors(context.Context, string, rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistoryContributor, error)
GetHistoryOverallStatus(context.Context, string, rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateWindow, error)
}
type Handler interface {
GetRuleHistoryStats(http.ResponseWriter, *http.Request)
GetRuleHistoryTimeline(http.ResponseWriter, *http.Request)
GetRuleHistoryFilterKeys(http.ResponseWriter, *http.Request)
GetRuleHistoryFilterValues(http.ResponseWriter, *http.Request)
GetRuleHistoryContributors(http.ResponseWriter, *http.Request)
GetRuleHistoryOverallStatus(http.ResponseWriter, *http.Request)
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
@@ -106,6 +107,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
signoz.TelemetryMetadataStore,
signoz.Prometheus,
signoz.Modules.OrgGetter,
signoz.Modules.RuleStateHistory,
signoz.Querier,
signoz.Instrumentation.ToProviderSettings(),
signoz.QueryParser,
@@ -336,6 +338,7 @@ func makeRulesManager(
metadataStore telemetrytypes.MetadataStore,
prometheus prometheus.Prometheus,
orgGetter organization.Getter,
ruleStateHistoryModule rulestatehistory.Module,
querier querier.Querier,
providerSettings factory.ProviderSettings,
queryParser queryparser.QueryParser,
@@ -344,22 +347,23 @@ func makeRulesManager(
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
// create manager opts
managerOpts := &rules.ManagerOptions{
TelemetryStore: telemetryStore,
MetadataStore: metadataStore,
Prometheus: prometheus,
Context: context.Background(),
Logger: zap.L(),
Reader: ch,
Querier: querier,
SLogger: providerSettings.Logger,
Cache: cache,
EvalDelay: constants.GetEvalDelay(),
OrgGetter: orgGetter,
Alertmanager: alertmanager,
RuleStore: ruleStore,
MaintenanceStore: maintenanceStore,
SqlStore: sqlstore,
QueryParser: queryParser,
TelemetryStore: telemetryStore,
MetadataStore: metadataStore,
Prometheus: prometheus,
Context: context.Background(),
Logger: zap.L(),
Reader: ch,
Querier: querier,
SLogger: providerSettings.Logger,
Cache: cache,
EvalDelay: constants.GetEvalDelay(),
OrgGetter: orgGetter,
Alertmanager: alertmanager,
RuleStore: ruleStore,
MaintenanceStore: maintenanceStore,
SqlStore: sqlstore,
QueryParser: queryParser,
RuleStateHistoryModule: ruleStateHistoryModule,
}
// create Manager

View File

@@ -8,6 +8,7 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
"github.com/SigNoz/signoz/pkg/query-service/model"
@@ -17,6 +18,7 @@ import (
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/sqlstore"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -96,6 +98,8 @@ type BaseRule struct {
// newGroupEvalDelay is the grace period for new alert groups
newGroupEvalDelay valuer.TextDuration
ruleStateHistoryModule rulestatehistory.Module
queryParser queryparser.QueryParser
}
@@ -143,6 +147,12 @@ func WithMetadataStore(metadataStore telemetrytypes.MetadataStore) RuleOption {
}
}
func WithRuleStateHistoryModule(module rulestatehistory.Module) RuleOption {
return func(r *BaseRule) {
r.ruleStateHistoryModule = module
}
}
func NewBaseRule(id string, orgID valuer.UUID, p *ruletypes.PostableRule, reader interfaces.Reader, opts ...RuleOption) (*BaseRule, error) {
if p.RuleCondition == nil || !p.RuleCondition.IsValid() {
return nil, fmt.Errorf("invalid rule condition")
@@ -400,100 +410,59 @@ func (r *BaseRule) ForEachActiveAlert(f func(*ruletypes.Alert)) {
}
func (r *BaseRule) RecordRuleStateHistory(ctx context.Context, prevState, currentState model.AlertState, itemsToAdd []model.RuleStateHistory) error {
zap.L().Debug("recording rule state history", zap.String("ruleid", r.ID()), zap.Any("prevState", prevState), zap.Any("currentState", currentState), zap.Any("itemsToAdd", itemsToAdd))
revisedItemsToAdd := map[uint64]model.RuleStateHistory{}
if r.ruleStateHistoryModule == nil {
return nil
}
lastSavedState, err := r.reader.GetLastSavedRuleStateHistory(ctx, r.ID())
if err != nil {
if err := r.ruleStateHistoryModule.RecordRuleStateHistory(ctx, r.ID(), r.handledRestart, toRuleStateHistoryTypes(itemsToAdd)); err != nil {
zap.L().Error("error while recording rule state history", zap.Error(err), zap.Any("itemsToAdd", itemsToAdd))
return err
}
// if the query-service has been restarted, or the rule has been modified (which re-initializes the rule),
// the state would reset so we need to add the corresponding state changes to previously saved states
if !r.handledRestart && len(lastSavedState) > 0 {
zap.L().Debug("handling restart", zap.String("ruleid", r.ID()), zap.Any("lastSavedState", lastSavedState))
l := map[uint64]model.RuleStateHistory{}
for _, item := range itemsToAdd {
l[item.Fingerprint] = item
}
shouldSkip := map[uint64]bool{}
for _, item := range lastSavedState {
// for the last saved item with fingerprint, check if there is a corresponding entry in the current state
currentState, ok := l[item.Fingerprint]
if !ok {
// there was a state change in the past, but not in the current state
// if the state was firing, then we should add a resolved state change
if item.State == model.StateFiring || item.State == model.StateNoData {
item.State = model.StateInactive
item.StateChanged = true
item.UnixMilli = time.Now().UnixMilli()
revisedItemsToAdd[item.Fingerprint] = item
}
// there is nothing to do if the prev state was normal
} else {
if item.State != currentState.State {
item.State = currentState.State
item.StateChanged = true
item.UnixMilli = time.Now().UnixMilli()
revisedItemsToAdd[item.Fingerprint] = item
}
}
// do not add this item to revisedItemsToAdd as it is already processed
shouldSkip[item.Fingerprint] = true
}
zap.L().Debug("after lastSavedState loop", zap.String("ruleid", r.ID()), zap.Any("revisedItemsToAdd", revisedItemsToAdd))
// if there are any new state changes that were not saved, add them to the revised items
for _, item := range itemsToAdd {
if _, ok := revisedItemsToAdd[item.Fingerprint]; !ok && !shouldSkip[item.Fingerprint] {
revisedItemsToAdd[item.Fingerprint] = item
}
}
zap.L().Debug("after itemsToAdd loop", zap.String("ruleid", r.ID()), zap.Any("revisedItemsToAdd", revisedItemsToAdd))
newState := model.StateInactive
for _, item := range revisedItemsToAdd {
if item.State == model.StateFiring || item.State == model.StateNoData {
newState = model.StateFiring
break
}
}
zap.L().Debug("newState", zap.String("ruleid", r.ID()), zap.Any("newState", newState))
// if there is a change in the overall state, update the overall state
if lastSavedState[0].OverallState != newState {
for fingerprint, item := range revisedItemsToAdd {
item.OverallState = newState
item.OverallStateChanged = true
revisedItemsToAdd[fingerprint] = item
}
}
zap.L().Debug("revisedItemsToAdd after newState", zap.String("ruleid", r.ID()), zap.Any("revisedItemsToAdd", revisedItemsToAdd))
} else {
for _, item := range itemsToAdd {
revisedItemsToAdd[item.Fingerprint] = item
}
}
if len(revisedItemsToAdd) > 0 && r.reader != nil {
zap.L().Debug("writing rule state history", zap.String("ruleid", r.ID()), zap.Any("revisedItemsToAdd", revisedItemsToAdd))
entries := make([]model.RuleStateHistory, 0, len(revisedItemsToAdd))
for _, item := range revisedItemsToAdd {
entries = append(entries, item)
}
err := r.reader.AddRuleStateHistory(ctx, entries)
if err != nil {
zap.L().Error("error while inserting rule state history", zap.Error(err), zap.Any("itemsToAdd", itemsToAdd))
}
}
r.handledRestart = true
return nil
}
// TODO(srikanthccv): remove these when v3 is cleaned up
func toRuleStateHistoryTypes(entries []model.RuleStateHistory) []rulestatehistorytypes.RuleStateHistory {
converted := make([]rulestatehistorytypes.RuleStateHistory, 0, len(entries))
for _, entry := range entries {
converted = append(converted, rulestatehistorytypes.RuleStateHistory{
RuleID: entry.RuleID,
RuleName: entry.RuleName,
OverallState: toRuleStateHistoryAlertState(entry.OverallState),
OverallStateChanged: entry.OverallStateChanged,
State: toRuleStateHistoryAlertState(entry.State),
StateChanged: entry.StateChanged,
UnixMilli: entry.UnixMilli,
Labels: rulestatehistorytypes.LabelsString(entry.Labels),
Fingerprint: entry.Fingerprint,
Value: entry.Value,
})
}
return converted
}
func toRuleStateHistoryAlertState(state model.AlertState) rulestatehistorytypes.AlertState {
switch state {
case model.StateInactive:
return rulestatehistorytypes.StateInactive
case model.StatePending:
return rulestatehistorytypes.StatePending
case model.StateRecovering:
return rulestatehistorytypes.StateRecovering
case model.StateFiring:
return rulestatehistorytypes.StateFiring
case model.StateNoData:
return rulestatehistorytypes.StateNoData
case model.StateDisabled:
return rulestatehistorytypes.StateDisabled
default:
return rulestatehistorytypes.StateInactive
}
}
func (r *BaseRule) PopulateTemporality(ctx context.Context, orgID valuer.UUID, qp *v3.QueryRangeParamsV3) error {
missingTemporality := make([]string, 0)
metricNameToTemporality := make(map[string]map[v3.Temporality]bool)

View File

@@ -20,6 +20,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/prometheus"
querierV5 "github.com/SigNoz/signoz/pkg/querier"
@@ -99,6 +100,8 @@ type ManagerOptions struct {
EvalDelay valuer.TextDuration
RuleStateHistoryModule rulestatehistory.Module
PrepareTaskFunc func(opts PrepareTaskOptions) (Task, error)
PrepareTestRuleFunc func(opts PrepareTestRuleOptions) (int, *model.ApiError)
Alertmanager alertmanager.Alertmanager
@@ -174,6 +177,7 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
WithSQLStore(opts.SQLStore),
WithQueryParser(opts.ManagerOpts.QueryParser),
WithMetadataStore(opts.ManagerOpts.MetadataStore),
WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
@@ -198,6 +202,7 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
WithSQLStore(opts.SQLStore),
WithQueryParser(opts.ManagerOpts.QueryParser),
WithMetadataStore(opts.ManagerOpts.MetadataStore),
WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {

View File

@@ -35,7 +35,7 @@ func TestThresholdRuleEvalBackwardCompat(t *testing.T) {
AlertName: "Eval Backward Compatibility Test without recovery target",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
EvalWindow: valuer.MustParseTextDuration("5m"),
Frequency: valuer.MustParseTextDuration("1m"),
}},
@@ -151,7 +151,7 @@ func TestPrepareLinksToLogs(t *testing.T) {
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeLogs,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
EvalWindow: valuer.MustParseTextDuration("5m"),
Frequency: valuer.MustParseTextDuration("1m"),
}},
@@ -205,7 +205,7 @@ func TestPrepareLinksToLogsV5(t *testing.T) {
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeLogs,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
EvalWindow: valuer.MustParseTextDuration("5m"),
Frequency: valuer.MustParseTextDuration("1m"),
}},
@@ -266,7 +266,7 @@ func TestPrepareLinksToTracesV5(t *testing.T) {
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeTraces,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
EvalWindow: valuer.MustParseTextDuration("5m"),
Frequency: valuer.MustParseTextDuration("1m"),
}},
@@ -327,7 +327,7 @@ func TestPrepareLinksToTraces(t *testing.T) {
AlertName: "Links to traces test",
AlertType: ruletypes.AlertTypeTraces,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
EvalWindow: valuer.MustParseTextDuration("5m"),
Frequency: valuer.MustParseTextDuration("1m"),
}},
@@ -381,7 +381,7 @@ func TestThresholdRuleLabelNormalization(t *testing.T) {
AlertName: "Tricky Condition Tests",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
EvalWindow: valuer.MustParseTextDuration("5m"),
Frequency: valuer.MustParseTextDuration("1m"),
}},

View File

@@ -22,6 +22,8 @@ import (
"github.com/SigNoz/signoz/pkg/modules/quickfilter/implquickfilter"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory/implrulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/savedview"
"github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
@@ -44,6 +46,7 @@ type Handlers struct {
QuickFilter quickfilter.Handler
TraceFunnel tracefunnel.Handler
RawDataExport rawdataexport.Handler
RuleStateHistory rulestatehistory.Handler
SpanPercentile spanpercentile.Handler
Services services.Handler
MetricsExplorer metricsexplorer.Handler
@@ -77,6 +80,7 @@ func NewHandlers(
QuickFilter: implquickfilter.NewHandler(modules.QuickFilter),
TraceFunnel: impltracefunnel.NewHandler(modules.TraceFunnel),
RawDataExport: implrawdataexport.NewHandler(modules.RawDataExport),
RuleStateHistory: implrulestatehistory.NewHandler(modules.RuleStateHistory),
Services: implservices.NewHandler(modules.Services),
MetricsExplorer: implmetricsexplorer.NewHandler(modules.MetricsExplorer),
SpanPercentile: implspanpercentile.NewHandler(modules.SpanPercentile),

View File

@@ -25,6 +25,8 @@ import (
"github.com/SigNoz/signoz/pkg/modules/quickfilter/implquickfilter"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory/implrulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/savedview"
"github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
@@ -51,24 +53,25 @@ import (
)
type Modules struct {
OrgGetter organization.Getter
OrgSetter organization.Setter
Preference preference.Module
User user.Module
UserGetter user.Getter
SavedView savedview.Module
Apdex apdex.Module
Dashboard dashboard.Module
QuickFilter quickfilter.Module
TraceFunnel tracefunnel.Module
RawDataExport rawdataexport.Module
AuthDomain authdomain.Module
Session session.Module
Services services.Module
SpanPercentile spanpercentile.Module
MetricsExplorer metricsexplorer.Module
Promote promote.Module
ServiceAccount serviceaccount.Module
OrgGetter organization.Getter
OrgSetter organization.Setter
Preference preference.Module
User user.Module
UserGetter user.Getter
SavedView savedview.Module
Apdex apdex.Module
Dashboard dashboard.Module
QuickFilter quickfilter.Module
TraceFunnel tracefunnel.Module
RawDataExport rawdataexport.Module
RuleStateHistory rulestatehistory.Module
AuthDomain authdomain.Module
Session session.Module
Services services.Module
SpanPercentile spanpercentile.Module
MetricsExplorer metricsexplorer.Module
Promote promote.Module
ServiceAccount serviceaccount.Module
}
func NewModules(
@@ -96,23 +99,24 @@ func NewModules(
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
return Modules{
OrgGetter: orgGetter,
OrgSetter: orgSetter,
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()),
SavedView: implsavedview.NewModule(sqlstore),
Apdex: implapdex.NewModule(sqlstore),
Dashboard: dashboard,
User: user,
UserGetter: userGetter,
QuickFilter: quickfilter,
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
RawDataExport: implrawdataexport.NewModule(querier),
AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs),
Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter),
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
Services: implservices.NewModule(querier, telemetryStore),
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
ServiceAccount: implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), authz, emailing, providerSettings),
OrgGetter: orgGetter,
OrgSetter: orgSetter,
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()),
SavedView: implsavedview.NewModule(sqlstore),
Apdex: implapdex.NewModule(sqlstore),
Dashboard: dashboard,
User: user,
UserGetter: userGetter,
QuickFilter: quickfilter,
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
RawDataExport: implrawdataexport.NewModule(querier),
RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger)),
AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs),
Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter),
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
Services: implservices.NewModule(querier, telemetryStore),
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
ServiceAccount: implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), authz, emailing, providerSettings),
}
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
@@ -61,6 +62,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ zeus.Handler }{},
struct{ querier.Handler }{},
struct{ serviceaccount.Handler }{},
struct{ rulestatehistory.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -256,6 +256,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.ZeusHandler,
handlers.QuerierHandler,
handlers.ServiceAccountHandler,
handlers.RuleStateHistory,
),
)
}

View File

@@ -0,0 +1,20 @@
package rulestatehistorytypes
import qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
// V2HistoryBaseQueryParams defines URL query params common across v2 rule history APIs.
type V2HistoryBaseQueryParams struct {
Start int64 `query:"start" required:"true"`
End int64 `query:"end" required:"true"`
}
// V2HistoryTimelineQueryParams defines URL query params for timeline API.
type V2HistoryTimelineQueryParams struct {
Start int64 `query:"start" required:"true"`
End int64 `query:"end" required:"true"`
State AlertState `query:"state"`
FilterExpression string `query:"filterExpression"`
Limit int64 `query:"limit"`
Order qbtypes.OrderDirection `query:"order"`
Cursor string `query:"cursor"`
}

View File

@@ -0,0 +1,35 @@
package rulestatehistorytypes
import (
"github.com/SigNoz/signoz/pkg/errors"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)
type Query struct {
Start int64
End int64
State AlertState
FilterExpression qbtypes.Filter
Limit int64
Offset int64
Order qbtypes.OrderDirection
}
func (q *Query) Validate() error {
if q.Start == 0 || q.End == 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "start and end are required")
}
if q.Start >= q.End {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "start must be less than end")
}
if q.Limit < 0 || q.Offset < 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "limit and offset must be greater than or equal to 0")
}
if q.Order.IsZero() {
q.Order = qbtypes.OrderDirectionDesc
}
if q.Order != qbtypes.OrderDirectionAsc && q.Order != qbtypes.OrderDirectionDesc {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order must be asc or desc")
}
return nil
}

View File

@@ -0,0 +1,49 @@
package rulestatehistorytypes
import (
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)
type RuleStateTimelineResponse struct {
Items []RuleStateHistoryResponseItem `json:"items" required:"true"`
Total uint64 `json:"total" required:"true"`
NextCursor string `json:"nextCursor,omitempty"`
}
type RuleStateHistoryResponseItem struct {
RuleID string `json:"ruleID" required:"true"`
RuleName string `json:"ruleName" required:"true"`
OverallState AlertState `json:"overallState" required:"true"`
OverallStateChanged bool `json:"overallStateChanged" required:"true"`
State AlertState `json:"state" required:"true"`
StateChanged bool `json:"stateChanged" required:"true"`
UnixMilli int64 `json:"unixMilli" required:"true"`
Labels []*qbtypes.Label `json:"labels" required:"true"`
Fingerprint uint64 `json:"fingerprint" required:"true"`
Value float64 `json:"value" required:"true"`
}
type RuleStateHistoryContributorResponse struct {
Fingerprint uint64 `json:"fingerprint" required:"true"`
Labels []*qbtypes.Label `json:"labels" required:"true"`
Count uint64 `json:"count" required:"true"`
RelatedTracesLink string `json:"relatedTracesLink,omitempty"`
RelatedLogsLink string `json:"relatedLogsLink,omitempty"`
}
type RuleStateWindow struct {
State AlertState `json:"state" ch:"state" required:"true"`
Start int64 `json:"start" ch:"start" required:"true"`
End int64 `json:"end" ch:"end" required:"true"`
}
type Stats struct {
TotalCurrentTriggers uint64 `json:"totalCurrentTriggers" required:"true"`
TotalPastTriggers uint64 `json:"totalPastTriggers" required:"true"`
CurrentTriggersSeries *qbtypes.TimeSeries `json:"currentTriggersSeries" required:"true" nullable:"true"`
PastTriggersSeries *qbtypes.TimeSeries `json:"pastTriggersSeries" required:"true" nullable:"true"`
CurrentAvgResolutionTime float64 `json:"currentAvgResolutionTime" required:"true"`
PastAvgResolutionTime float64 `json:"pastAvgResolutionTime" required:"true"`
CurrentAvgResolutionTimeSeries *qbtypes.TimeSeries `json:"currentAvgResolutionTimeSeries" required:"true" nullable:"true"`
PastAvgResolutionTimeSeries *qbtypes.TimeSeries `json:"pastAvgResolutionTimeSeries" required:"true" nullable:"true"`
}

View File

@@ -0,0 +1,92 @@
package rulestatehistorytypes
import (
"context"
"database/sql/driver"
"github.com/SigNoz/signoz/pkg/errors"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type AlertState struct {
valuer.String
}
var (
StateInactive = AlertState{valuer.NewString("inactive")}
StatePending = AlertState{valuer.NewString("pending")}
StateRecovering = AlertState{valuer.NewString("recovering")}
StateFiring = AlertState{valuer.NewString("firing")}
StateNoData = AlertState{valuer.NewString("nodata")}
StateDisabled = AlertState{valuer.NewString("disabled")}
)
type LabelsString string
func (AlertState) Enum() []any {
return []any{
StateInactive,
StatePending,
StateRecovering,
StateFiring,
StateNoData,
StateDisabled,
}
}
func (l *LabelsString) Scan(src any) error {
switch data := src.(type) {
case nil:
*l = ""
case string:
*l = LabelsString(data)
case []byte:
*l = LabelsString(string(data))
default:
return errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported labels type")
}
return nil
}
func (l LabelsString) Value() (driver.Value, error) {
return string(l), nil
}
type RuleStateHistory struct {
RuleID string `ch:"rule_id"`
RuleName string `ch:"rule_name"`
OverallState AlertState `ch:"overall_state"`
OverallStateChanged bool `ch:"overall_state_changed"`
State AlertState `ch:"state"`
StateChanged bool `ch:"state_changed"`
UnixMilli int64 `ch:"unix_milli"`
Labels LabelsString `ch:"labels"`
Fingerprint uint64 `ch:"fingerprint"`
Value float64 `ch:"value"`
}
type RuleStateHistoryContributor struct {
Fingerprint uint64 `ch:"fingerprint"`
Labels LabelsString `ch:"labels"`
Count uint64 `ch:"count"`
RelatedTracesLink string
RelatedLogsLink string
}
type Store interface {
AddRuleStateHistory(ctx context.Context, entries []RuleStateHistory) error
GetLastSavedRuleStateHistory(ctx context.Context, ruleID string) ([]RuleStateHistory, error)
ReadRuleStateHistoryByRuleID(ctx context.Context, ruleID string, query *Query) ([]RuleStateHistory, uint64, error)
ReadRuleStateHistoryFilterKeysByRuleID(ctx context.Context, ruleID string, query *Query, search string, limit int64) (*telemetrytypes.GettableFieldKeys, error)
ReadRuleStateHistoryFilterValuesByRuleID(ctx context.Context, ruleID string, key string, query *Query, search string, limit int64) (*telemetrytypes.GettableFieldValues, error)
ReadRuleStateHistoryTopContributorsByRuleID(ctx context.Context, ruleID string, query *Query) ([]RuleStateHistoryContributor, error)
GetOverallStateTransitions(ctx context.Context, ruleID string, query *Query) ([]RuleStateWindow, error)
GetTotalTriggers(ctx context.Context, ruleID string, query *Query) (uint64, error)
GetTriggersByInterval(ctx context.Context, ruleID string, query *Query) (*qbtypes.TimeSeries, error)
GetAvgResolutionTime(ctx context.Context, ruleID string, query *Query) (float64, error)
GetAvgResolutionTimeByInterval(ctx context.Context, ruleID string, query *Query) (*qbtypes.TimeSeries, error)
}