mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-17 17:30:31 +01:00
Compare commits
34 Commits
ns/td-cach
...
platform-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c2f8f5348 | ||
|
|
4d098f5b16 | ||
|
|
d24cc57514 | ||
|
|
5c6e511f94 | ||
|
|
84bb865ef9 | ||
|
|
634d236d19 | ||
|
|
950c5f2f5d | ||
|
|
a63b99ec8d | ||
|
|
4fe932a7a5 | ||
|
|
bb8421fbbf | ||
|
|
c8814833a8 | ||
|
|
0be93850ee | ||
|
|
f4d2acd7f5 | ||
|
|
cbfb23ef54 | ||
|
|
0c73afffb8 | ||
|
|
eb01eebd17 | ||
|
|
df143939b1 | ||
|
|
8bd4b48ff2 | ||
|
|
0365d82233 | ||
|
|
8827a34011 | ||
|
|
b068a45462 | ||
|
|
c416393855 | ||
|
|
338814958b | ||
|
|
080eae1c77 | ||
|
|
419ec1bb58 | ||
|
|
ec288eb222 | ||
|
|
fdefad1fa0 | ||
|
|
fdbab31f88 | ||
|
|
bd3943cac3 | ||
|
|
6b5f6484b5 | ||
|
|
f771e9fb3c | ||
|
|
6fc32144dc | ||
|
|
b5c146afdf | ||
|
|
dfbcd1e0ec |
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/SigNoz/signoz/cmd"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
|
||||
"github.com/SigNoz/signoz/pkg/authz/openfgaschema"
|
||||
"github.com/SigNoz/signoz/pkg/authz/openfgaserver"
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/gateway"
|
||||
@@ -26,14 +28,20 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
"github.com/SigNoz/signoz/pkg/zeus/noopzeus"
|
||||
@@ -107,6 +115,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(_ sqlstore.SQLStore, _ global.Global, _ zeus.Zeus, _ gateway.Gateway, _ licensing.Licensing, _ serviceaccount.Module, _ cloudintegration.Config) (cloudintegration.Module, error) {
|
||||
return implcloudintegration.NewModule(), nil
|
||||
},
|
||||
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
|
||||
return factory.MustNewNamedMap(signozruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, nil, nil))
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err))
|
||||
|
||||
@@ -22,14 +22,17 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
|
||||
eequerier "github.com/SigNoz/signoz/ee/querier"
|
||||
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
|
||||
eerules "github.com/SigNoz/signoz/ee/query-service/rules"
|
||||
"github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema"
|
||||
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
||||
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
|
||||
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/gateway"
|
||||
@@ -40,15 +43,21 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
)
|
||||
@@ -166,6 +175,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
|
||||
return implcloudintegration.NewModule(pkgcloudintegration.NewStore(sqlStore), global, zeus, gateway, licensing, serviceAccount, cloudProvidersMap, config)
|
||||
},
|
||||
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
|
||||
return factory.MustNewNamedMap(signozruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, eerules.PrepareTaskFunc, eerules.TestNotification))
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err))
|
||||
|
||||
1088
docs/api/openapi.yml
1088
docs/api/openapi.yml
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
@@ -23,7 +22,6 @@ import (
|
||||
|
||||
type APIHandlerOptions struct {
|
||||
DataConnector interfaces.Reader
|
||||
RulesManager *rules.Manager
|
||||
UsageManager *usage.Manager
|
||||
IntegrationsController *integrations.Controller
|
||||
CloudIntegrationsController *cloudintegrations.Controller
|
||||
@@ -43,7 +41,6 @@ type APIHandler struct {
|
||||
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.Config) (*APIHandler, error) {
|
||||
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
||||
Reader: opts.DataConnector,
|
||||
RuleManager: opts.RulesManager,
|
||||
IntegrationsController: opts.IntegrationsController,
|
||||
CloudIntegrationsController: opts.CloudIntegrationsController,
|
||||
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
||||
@@ -64,10 +61,6 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
|
||||
return ah, nil
|
||||
}
|
||||
|
||||
func (ah *APIHandler) RM() *rules.Manager {
|
||||
return ah.opts.RulesManager
|
||||
}
|
||||
|
||||
func (ah *APIHandler) UM() *usage.Manager {
|
||||
return ah.opts.UsageManager
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/cache/memorycache"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
|
||||
@@ -23,18 +19,10 @@ import (
|
||||
"github.com/soheilhy/cmux"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/app/api"
|
||||
"github.com/SigNoz/signoz/ee/query-service/rules"
|
||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"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"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/web"
|
||||
|
||||
"log/slog"
|
||||
@@ -49,7 +37,6 @@ import (
|
||||
opAmpModel "github.com/SigNoz/signoz/pkg/query-service/app/opamp/model"
|
||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
)
|
||||
|
||||
@@ -57,7 +44,6 @@ import (
|
||||
type Server struct {
|
||||
config signoz.Config
|
||||
signoz *signoz.SigNoz
|
||||
ruleManager *baserules.Manager
|
||||
|
||||
// public http router
|
||||
httpConn net.Listener
|
||||
@@ -97,24 +83,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
nil,
|
||||
)
|
||||
|
||||
rm, err := makeRulesManager(
|
||||
signoz.Cache,
|
||||
signoz.Alertmanager,
|
||||
signoz.SQLStore,
|
||||
signoz.TelemetryStore,
|
||||
signoz.TelemetryMetadataStore,
|
||||
signoz.Prometheus,
|
||||
signoz.Modules.OrgGetter,
|
||||
signoz.Modules.RuleStateHistory,
|
||||
signoz.Querier,
|
||||
signoz.Instrumentation.ToProviderSettings(),
|
||||
signoz.QueryParser,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// initiate opamp
|
||||
opAmpModel.Init(signoz.SQLStore, signoz.Instrumentation.Logger(), signoz.Modules.OrgGetter)
|
||||
|
||||
@@ -163,7 +131,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
|
||||
apiOpts := api.APIHandlerOptions{
|
||||
DataConnector: reader,
|
||||
RulesManager: rm,
|
||||
UsageManager: usageManager,
|
||||
IntegrationsController: integrationsController,
|
||||
CloudIntegrationsController: cloudIntegrationsController,
|
||||
@@ -180,8 +147,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
|
||||
s := &Server{
|
||||
config: config,
|
||||
signoz: signoz,
|
||||
ruleManager: rm,
|
||||
signoz: signoz,
|
||||
httpHostPort: baseconst.HTTPHostPort,
|
||||
unavailableChannel: make(chan healthcheck.Status),
|
||||
usageManager: usageManager,
|
||||
@@ -288,8 +254,6 @@ func (s *Server) initListeners() error {
|
||||
|
||||
// Start listening on http and private http port concurrently
|
||||
func (s *Server) Start(ctx context.Context) error {
|
||||
s.ruleManager.Start(ctx)
|
||||
|
||||
err := s.initListeners()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -333,47 +297,9 @@ func (s *Server) Stop(ctx context.Context) error {
|
||||
|
||||
s.opampServer.Stop()
|
||||
|
||||
if s.ruleManager != nil {
|
||||
s.ruleManager.Stop(ctx)
|
||||
}
|
||||
|
||||
// stop usage manager
|
||||
s.usageManager.Stop(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeRulesManager(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(),
|
||||
Querier: querier,
|
||||
Logger: 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
|
||||
manager, err := baserules.NewManager(managerOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rule manager error: %v", err)
|
||||
}
|
||||
|
||||
slog.Info("rules manager is ready")
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
@@ -48,22 +48,22 @@
|
||||
"@radix-ui/react-tooltip": "1.0.7",
|
||||
"@sentry/react": "8.41.0",
|
||||
"@sentry/vite-plugin": "2.22.6",
|
||||
"@signozhq/button": "0.0.2",
|
||||
"@signozhq/calendar": "0.0.0",
|
||||
"@signozhq/callout": "0.0.2",
|
||||
"@signozhq/checkbox": "0.0.2",
|
||||
"@signozhq/combobox": "0.0.2",
|
||||
"@signozhq/command": "0.0.0",
|
||||
"@signozhq/button": "0.0.5",
|
||||
"@signozhq/calendar": "0.1.1",
|
||||
"@signozhq/callout": "0.0.4",
|
||||
"@signozhq/checkbox": "0.0.4",
|
||||
"@signozhq/combobox": "0.0.4",
|
||||
"@signozhq/command": "0.0.2",
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/dialog": "^0.0.2",
|
||||
"@signozhq/drawer": "0.0.4",
|
||||
"@signozhq/dialog": "0.0.4",
|
||||
"@signozhq/drawer": "0.0.6",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
"@signozhq/radio-group": "0.0.2",
|
||||
"@signozhq/resizable": "0.0.0",
|
||||
"@signozhq/input": "0.0.4",
|
||||
"@signozhq/popover": "0.1.2",
|
||||
"@signozhq/radio-group": "0.0.4",
|
||||
"@signozhq/resizable": "0.0.2",
|
||||
"@signozhq/table": "0.3.7",
|
||||
"@signozhq/toggle-group": "0.0.1",
|
||||
"@signozhq/toggle-group": "0.0.3",
|
||||
"@signozhq/ui": "0.0.5",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
|
||||
495
frontend/src/api/generated/services/downtimeschedules/index.ts
Normal file
495
frontend/src/api/generated/services/downtimeschedules/index.ts
Normal file
@@ -0,0 +1,495 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'yarn generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type {
|
||||
DeleteDowntimeScheduleByIDPathParameters,
|
||||
GetDowntimeScheduleByID200,
|
||||
GetDowntimeScheduleByIDPathParameters,
|
||||
ListDowntimeSchedules200,
|
||||
ListDowntimeSchedulesParams,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesGettablePlannedMaintenanceDTO,
|
||||
UpdateDowntimeScheduleByIDPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
/**
|
||||
* This endpoint lists all planned maintenance / downtime schedules
|
||||
* @summary List downtime schedules
|
||||
*/
|
||||
export const listDowntimeSchedules = (
|
||||
params?: ListDowntimeSchedulesParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListDowntimeSchedules200>({
|
||||
url: `/api/v1/downtime_schedules`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListDowntimeSchedulesQueryKey = (
|
||||
params?: ListDowntimeSchedulesParams,
|
||||
) => {
|
||||
return [`/api/v1/downtime_schedules`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getListDowntimeSchedulesQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listDowntimeSchedules>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
params?: ListDowntimeSchedulesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listDowntimeSchedules>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getListDowntimeSchedulesQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof listDowntimeSchedules>>
|
||||
> = ({ signal }) => listDowntimeSchedules(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listDowntimeSchedules>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListDowntimeSchedulesQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listDowntimeSchedules>>
|
||||
>;
|
||||
export type ListDowntimeSchedulesQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List downtime schedules
|
||||
*/
|
||||
|
||||
export function useListDowntimeSchedules<
|
||||
TData = Awaited<ReturnType<typeof listDowntimeSchedules>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
params?: ListDowntimeSchedulesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listDowntimeSchedules>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListDowntimeSchedulesQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List downtime schedules
|
||||
*/
|
||||
export const invalidateListDowntimeSchedules = async (
|
||||
queryClient: QueryClient,
|
||||
params?: ListDowntimeSchedulesParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListDowntimeSchedulesQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint creates a new planned maintenance / downtime schedule
|
||||
* @summary Create downtime schedule
|
||||
*/
|
||||
export const createDowntimeSchedule = (
|
||||
ruletypesGettablePlannedMaintenanceDTO: BodyType<RuletypesGettablePlannedMaintenanceDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/downtime_schedules`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: ruletypesGettablePlannedMaintenanceDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateDowntimeScheduleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesGettablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesGettablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createDowntimeSchedule'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
{ data: BodyType<RuletypesGettablePlannedMaintenanceDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createDowntimeSchedule(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateDowntimeScheduleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>
|
||||
>;
|
||||
export type CreateDowntimeScheduleMutationBody = BodyType<RuletypesGettablePlannedMaintenanceDTO>;
|
||||
export type CreateDowntimeScheduleMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create downtime schedule
|
||||
*/
|
||||
export const useCreateDowntimeSchedule = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesGettablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesGettablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getCreateDowntimeScheduleMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint deletes a downtime schedule by ID
|
||||
* @summary Delete downtime schedule
|
||||
*/
|
||||
export const deleteDowntimeScheduleByID = ({
|
||||
id,
|
||||
}: DeleteDowntimeScheduleByIDPathParameters) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/downtime_schedules/${id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteDowntimeScheduleByIDMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteDowntimeScheduleByID>>,
|
||||
TError,
|
||||
{ pathParams: DeleteDowntimeScheduleByIDPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteDowntimeScheduleByID>>,
|
||||
TError,
|
||||
{ pathParams: DeleteDowntimeScheduleByIDPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteDowntimeScheduleByID'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof deleteDowntimeScheduleByID>>,
|
||||
{ pathParams: DeleteDowntimeScheduleByIDPathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteDowntimeScheduleByID(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteDowntimeScheduleByIDMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteDowntimeScheduleByID>>
|
||||
>;
|
||||
|
||||
export type DeleteDowntimeScheduleByIDMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete downtime schedule
|
||||
*/
|
||||
export const useDeleteDowntimeScheduleByID = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteDowntimeScheduleByID>>,
|
||||
TError,
|
||||
{ pathParams: DeleteDowntimeScheduleByIDPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteDowntimeScheduleByID>>,
|
||||
TError,
|
||||
{ pathParams: DeleteDowntimeScheduleByIDPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getDeleteDowntimeScheduleByIDMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint returns a downtime schedule by ID
|
||||
* @summary Get downtime schedule by ID
|
||||
*/
|
||||
export const getDowntimeScheduleByID = (
|
||||
{ id }: GetDowntimeScheduleByIDPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetDowntimeScheduleByID200>({
|
||||
url: `/api/v1/downtime_schedules/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetDowntimeScheduleByIDQueryKey = ({
|
||||
id,
|
||||
}: GetDowntimeScheduleByIDPathParameters) => {
|
||||
return [`/api/v1/downtime_schedules/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetDowntimeScheduleByIDQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getDowntimeScheduleByID>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetDowntimeScheduleByIDPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getDowntimeScheduleByID>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetDowntimeScheduleByIDQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getDowntimeScheduleByID>>
|
||||
> = ({ signal }) => getDowntimeScheduleByID({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getDowntimeScheduleByID>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetDowntimeScheduleByIDQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getDowntimeScheduleByID>>
|
||||
>;
|
||||
export type GetDowntimeScheduleByIDQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get downtime schedule by ID
|
||||
*/
|
||||
|
||||
export function useGetDowntimeScheduleByID<
|
||||
TData = Awaited<ReturnType<typeof getDowntimeScheduleByID>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetDowntimeScheduleByIDPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getDowntimeScheduleByID>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetDowntimeScheduleByIDQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get downtime schedule by ID
|
||||
*/
|
||||
export const invalidateGetDowntimeScheduleByID = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetDowntimeScheduleByIDPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetDowntimeScheduleByIDQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint updates a downtime schedule by ID
|
||||
* @summary Update downtime schedule
|
||||
*/
|
||||
export const updateDowntimeScheduleByID = (
|
||||
{ id }: UpdateDowntimeScheduleByIDPathParameters,
|
||||
ruletypesGettablePlannedMaintenanceDTO: BodyType<RuletypesGettablePlannedMaintenanceDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/downtime_schedules/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: ruletypesGettablePlannedMaintenanceDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateDowntimeScheduleByIDMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data: BodyType<RuletypesGettablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data: BodyType<RuletypesGettablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateDowntimeScheduleByID'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data: BodyType<RuletypesGettablePlannedMaintenanceDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateDowntimeScheduleByID(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateDowntimeScheduleByIDMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>
|
||||
>;
|
||||
export type UpdateDowntimeScheduleByIDMutationBody = BodyType<RuletypesGettablePlannedMaintenanceDTO>;
|
||||
export type UpdateDowntimeScheduleByIDMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update downtime schedule
|
||||
*/
|
||||
export const useUpdateDowntimeScheduleByID = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data: BodyType<RuletypesGettablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data: BodyType<RuletypesGettablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getUpdateDowntimeScheduleByIDMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
@@ -6,17 +6,24 @@
|
||||
*/
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import type { ErrorType } from '../../../generatedAPIInstance';
|
||||
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type {
|
||||
CreateRule201,
|
||||
DeleteRuleByIDPathParameters,
|
||||
GetRuleByID200,
|
||||
GetRuleByIDPathParameters,
|
||||
GetRuleHistoryFilterKeys200,
|
||||
GetRuleHistoryFilterKeysParams,
|
||||
GetRuleHistoryFilterKeysPathParameters,
|
||||
@@ -35,9 +42,548 @@ import type {
|
||||
GetRuleHistoryTopContributors200,
|
||||
GetRuleHistoryTopContributorsParams,
|
||||
GetRuleHistoryTopContributorsPathParameters,
|
||||
ListRules200,
|
||||
PatchRuleByID200,
|
||||
PatchRuleByIDPathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesPostableRuleDTO,
|
||||
TestRule200,
|
||||
UpdateRuleByIDPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
/**
|
||||
* This endpoint lists all alert rules with their current evaluation state
|
||||
* @summary List alert rules
|
||||
*/
|
||||
export const listRules = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<ListRules200>({
|
||||
url: `/api/v2/rules`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListRulesQueryKey = () => {
|
||||
return [`/api/v2/rules`] as const;
|
||||
};
|
||||
|
||||
export const getListRulesQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listRules>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof listRules>>, TError, TData>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListRulesQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listRules>>> = ({
|
||||
signal,
|
||||
}) => listRules(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listRules>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListRulesQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listRules>>
|
||||
>;
|
||||
export type ListRulesQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List alert rules
|
||||
*/
|
||||
|
||||
export function useListRules<
|
||||
TData = Awaited<ReturnType<typeof listRules>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof listRules>>, TError, TData>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListRulesQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List alert rules
|
||||
*/
|
||||
export const invalidateListRules = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListRulesQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint creates a new alert rule
|
||||
* @summary Create alert rule
|
||||
*/
|
||||
export const createRule = (
|
||||
ruletypesPostableRuleDTO: BodyType<RuletypesPostableRuleDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateRule201>({
|
||||
url: `/api/v2/rules`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: ruletypesPostableRuleDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateRuleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createRule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createRule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createRule'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createRule>>,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createRule(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateRuleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createRule>>
|
||||
>;
|
||||
export type CreateRuleMutationBody = BodyType<RuletypesPostableRuleDTO>;
|
||||
export type CreateRuleMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create alert rule
|
||||
*/
|
||||
export const useCreateRule = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createRule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createRule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getCreateRuleMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint deletes an alert rule by ID
|
||||
* @summary Delete alert rule
|
||||
*/
|
||||
export const deleteRuleByID = ({ id }: DeleteRuleByIDPathParameters) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v2/rules/${id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteRuleByIDMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteRuleByID>>,
|
||||
TError,
|
||||
{ pathParams: DeleteRuleByIDPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteRuleByID>>,
|
||||
TError,
|
||||
{ pathParams: DeleteRuleByIDPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteRuleByID'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof deleteRuleByID>>,
|
||||
{ pathParams: DeleteRuleByIDPathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteRuleByID(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteRuleByIDMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteRuleByID>>
|
||||
>;
|
||||
|
||||
export type DeleteRuleByIDMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete alert rule
|
||||
*/
|
||||
export const useDeleteRuleByID = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteRuleByID>>,
|
||||
TError,
|
||||
{ pathParams: DeleteRuleByIDPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteRuleByID>>,
|
||||
TError,
|
||||
{ pathParams: DeleteRuleByIDPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getDeleteRuleByIDMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint returns an alert rule by ID
|
||||
* @summary Get alert rule by ID
|
||||
*/
|
||||
export const getRuleByID = (
|
||||
{ id }: GetRuleByIDPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetRuleByID200>({
|
||||
url: `/api/v2/rules/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetRuleByIDQueryKey = ({ id }: GetRuleByIDPathParameters) => {
|
||||
return [`/api/v2/rules/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetRuleByIDQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getRuleByID>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetRuleByIDPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleByID>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetRuleByIDQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getRuleByID>>> = ({
|
||||
signal,
|
||||
}) => getRuleByID({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleByID>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetRuleByIDQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getRuleByID>>
|
||||
>;
|
||||
export type GetRuleByIDQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get alert rule by ID
|
||||
*/
|
||||
|
||||
export function useGetRuleByID<
|
||||
TData = Awaited<ReturnType<typeof getRuleByID>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetRuleByIDPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleByID>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetRuleByIDQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get alert rule by ID
|
||||
*/
|
||||
export const invalidateGetRuleByID = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetRuleByIDPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetRuleByIDQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint applies a partial update to an alert rule by ID
|
||||
* @summary Patch alert rule
|
||||
*/
|
||||
export const patchRuleByID = (
|
||||
{ id }: PatchRuleByIDPathParameters,
|
||||
ruletypesPostableRuleDTO: BodyType<RuletypesPostableRuleDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<PatchRuleByID200>({
|
||||
url: `/api/v2/rules/${id}`,
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: ruletypesPostableRuleDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPatchRuleByIDMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchRuleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchRuleByIDPathParameters;
|
||||
data: BodyType<RuletypesPostableRuleDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchRuleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchRuleByIDPathParameters;
|
||||
data: BodyType<RuletypesPostableRuleDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['patchRuleByID'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof patchRuleByID>>,
|
||||
{
|
||||
pathParams: PatchRuleByIDPathParameters;
|
||||
data: BodyType<RuletypesPostableRuleDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return patchRuleByID(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type PatchRuleByIDMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof patchRuleByID>>
|
||||
>;
|
||||
export type PatchRuleByIDMutationBody = BodyType<RuletypesPostableRuleDTO>;
|
||||
export type PatchRuleByIDMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Patch alert rule
|
||||
*/
|
||||
export const usePatchRuleByID = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchRuleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchRuleByIDPathParameters;
|
||||
data: BodyType<RuletypesPostableRuleDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof patchRuleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchRuleByIDPathParameters;
|
||||
data: BodyType<RuletypesPostableRuleDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getPatchRuleByIDMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint updates an alert rule by ID
|
||||
* @summary Update alert rule
|
||||
*/
|
||||
export const updateRuleByID = (
|
||||
{ id }: UpdateRuleByIDPathParameters,
|
||||
ruletypesPostableRuleDTO: BodyType<RuletypesPostableRuleDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v2/rules/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: ruletypesPostableRuleDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateRuleByIDMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateRuleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateRuleByIDPathParameters;
|
||||
data: BodyType<RuletypesPostableRuleDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateRuleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateRuleByIDPathParameters;
|
||||
data: BodyType<RuletypesPostableRuleDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateRuleByID'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof updateRuleByID>>,
|
||||
{
|
||||
pathParams: UpdateRuleByIDPathParameters;
|
||||
data: BodyType<RuletypesPostableRuleDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateRuleByID(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateRuleByIDMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateRuleByID>>
|
||||
>;
|
||||
export type UpdateRuleByIDMutationBody = BodyType<RuletypesPostableRuleDTO>;
|
||||
export type UpdateRuleByIDMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update alert rule
|
||||
*/
|
||||
export const useUpdateRuleByID = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateRuleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateRuleByIDPathParameters;
|
||||
data: BodyType<RuletypesPostableRuleDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateRuleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateRuleByIDPathParameters;
|
||||
data: BodyType<RuletypesPostableRuleDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getUpdateRuleByIDMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Returns distinct label keys from rule history entries for the selected range.
|
||||
* @summary Get rule history filter keys
|
||||
@@ -742,3 +1288,87 @@ export const invalidateGetRuleHistoryTopContributors = async (
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint fires a test notification for the given rule definition
|
||||
* @summary Test alert rule
|
||||
*/
|
||||
export const testRule = (
|
||||
ruletypesPostableRuleDTO: BodyType<RuletypesPostableRuleDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<TestRule200>({
|
||||
url: `/api/v2/rules/test`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: ruletypesPostableRuleDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getTestRuleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testRule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testRule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['testRule'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof testRule>>,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return testRule(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type TestRuleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof testRule>>
|
||||
>;
|
||||
export type TestRuleMutationBody = BodyType<RuletypesPostableRuleDTO>;
|
||||
export type TestRuleMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Test alert rule
|
||||
*/
|
||||
export const useTestRule = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testRule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof testRule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getTestRuleMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
|
||||
@@ -4529,6 +4529,20 @@ export interface RulestatehistorytypesGettableRuleStateWindowDTO {
|
||||
state: RuletypesAlertStateDTO;
|
||||
}
|
||||
|
||||
export interface RuletypesAlertCompositeQueryDTO {
|
||||
panelType: RuletypesPanelTypeDTO;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
queries: Querybuildertypesv5QueryEnvelopeDTO[] | null;
|
||||
queryType: RuletypesQueryTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export enum RuletypesAlertStateDTO {
|
||||
inactive = 'inactive',
|
||||
pending = 'pending',
|
||||
@@ -4537,6 +4551,494 @@ export enum RuletypesAlertStateDTO {
|
||||
nodata = 'nodata',
|
||||
disabled = 'disabled',
|
||||
}
|
||||
export interface RuletypesBasicRuleThresholdDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
channels?: string[] | null;
|
||||
matchType: RuletypesMatchTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
op: RuletypesCompareOperatorDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @nullable true
|
||||
*/
|
||||
recoveryTarget?: number | null;
|
||||
/**
|
||||
* @type number
|
||||
* @nullable true
|
||||
*/
|
||||
target: number | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
targetUnit?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type RuletypesBasicRuleThresholdsDTO =
|
||||
| RuletypesBasicRuleThresholdDTO[]
|
||||
| null;
|
||||
|
||||
export enum RuletypesCompareOperatorDTO {
|
||||
above = 'above',
|
||||
below = 'below',
|
||||
equal = 'equal',
|
||||
not_equal = 'not_equal',
|
||||
outside_bounds = 'outside_bounds',
|
||||
}
|
||||
export interface RuletypesCumulativeScheduleDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @nullable true
|
||||
*/
|
||||
day?: number | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @nullable true
|
||||
*/
|
||||
hour?: number | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @nullable true
|
||||
*/
|
||||
minute?: number | null;
|
||||
type?: RuletypesScheduleTypeDTO;
|
||||
/**
|
||||
* @type integer
|
||||
* @nullable true
|
||||
*/
|
||||
weekday?: number | null;
|
||||
}
|
||||
|
||||
export interface RuletypesCumulativeWindowDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
frequency: string;
|
||||
schedule: RuletypesCumulativeScheduleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export interface RuletypesEvaluationCumulativeDTO {
|
||||
kind?: RuletypesEvaluationKindDTO;
|
||||
spec?: RuletypesCumulativeWindowDTO;
|
||||
}
|
||||
|
||||
export type RuletypesEvaluationEnvelopeDTO =
|
||||
| (RuletypesEvaluationRollingDTO & {
|
||||
kind: RuletypesEvaluationKindDTO;
|
||||
spec: unknown;
|
||||
})
|
||||
| (RuletypesEvaluationCumulativeDTO & {
|
||||
kind: RuletypesEvaluationKindDTO;
|
||||
spec: unknown;
|
||||
});
|
||||
|
||||
export enum RuletypesEvaluationKindDTO {
|
||||
rolling = 'rolling',
|
||||
cumulative = 'cumulative',
|
||||
}
|
||||
export interface RuletypesEvaluationRollingDTO {
|
||||
kind?: RuletypesEvaluationKindDTO;
|
||||
spec?: RuletypesRollingWindowDTO;
|
||||
}
|
||||
|
||||
export interface RuletypesGettablePlannedMaintenanceDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
alertIds?: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
kind?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
schedule: RuletypesScheduleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export type RuletypesGettableRuleDTOAnnotations = { [key: string]: string };
|
||||
|
||||
export type RuletypesGettableRuleDTOLabels = { [key: string]: string };
|
||||
|
||||
export interface RuletypesGettableRuleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
alert: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
alertType?: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
annotations?: RuletypesGettableRuleDTOAnnotations;
|
||||
condition: RuletypesRuleConditionDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
* @nullable true
|
||||
*/
|
||||
createAt?: Date | null;
|
||||
/**
|
||||
* @type string
|
||||
* @nullable true
|
||||
*/
|
||||
createBy?: string | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
evalWindow?: string;
|
||||
evaluation?: RuletypesEvaluationEnvelopeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
frequency?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
labels?: RuletypesGettableRuleDTOLabels;
|
||||
notificationSettings?: RuletypesNotificationSettingsDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
preferredChannels?: string[];
|
||||
ruleType: RuletypesRuleTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
source?: string;
|
||||
state?: RuletypesAlertStateDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
* @nullable true
|
||||
*/
|
||||
updateAt?: Date | null;
|
||||
/**
|
||||
* @type string
|
||||
* @nullable true
|
||||
*/
|
||||
updateBy?: string | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface RuletypesGettableRulesDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
rules?: RuletypesGettableRuleDTO[] | null;
|
||||
}
|
||||
|
||||
export interface RuletypesGettableTestRuleDTO {
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
alertCount?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export enum RuletypesMatchTypeDTO {
|
||||
at_least_once = 'at_least_once',
|
||||
all_the_times = 'all_the_times',
|
||||
on_average = 'on_average',
|
||||
in_total = 'in_total',
|
||||
last = 'last',
|
||||
}
|
||||
export interface RuletypesNotificationSettingsDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
groupBy?: string[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
newGroupEvalDelay?: string;
|
||||
renotify?: RuletypesRenotifyDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
usePolicy?: boolean;
|
||||
}
|
||||
|
||||
export enum RuletypesPanelTypeDTO {
|
||||
value = 'value',
|
||||
table = 'table',
|
||||
graph = 'graph',
|
||||
}
|
||||
export type RuletypesPostableRuleDTOAnnotations = { [key: string]: string };
|
||||
|
||||
export type RuletypesPostableRuleDTOLabels = { [key: string]: string };
|
||||
|
||||
export interface RuletypesPostableRuleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
alert: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
alertType?: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
annotations?: RuletypesPostableRuleDTOAnnotations;
|
||||
condition: RuletypesRuleConditionDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
evalWindow?: string;
|
||||
evaluation?: RuletypesEvaluationEnvelopeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
frequency?: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
labels?: RuletypesPostableRuleDTOLabels;
|
||||
notificationSettings?: RuletypesNotificationSettingsDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
preferredChannels?: string[];
|
||||
ruleType: RuletypesRuleTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
source?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export enum RuletypesQueryTypeDTO {
|
||||
builder = 'builder',
|
||||
clickhouse_sql = 'clickhouse_sql',
|
||||
promql = 'promql',
|
||||
}
|
||||
export interface RuletypesRecurrenceDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
duration?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
* @nullable true
|
||||
*/
|
||||
endTime?: Date | null;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
repeatOn?: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
repeatType?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
startTime?: Date;
|
||||
}
|
||||
|
||||
export interface RuletypesRenotifyDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
alertStates?: RuletypesAlertStateDTO[];
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
interval?: string;
|
||||
}
|
||||
|
||||
export interface RuletypesRollingWindowDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
evalWindow: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
frequency: string;
|
||||
}
|
||||
|
||||
export interface RuletypesRuleConditionDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
absentFor?: number;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
alertOnAbsent?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
algorithm?: string;
|
||||
compositeQuery: RuletypesAlertCompositeQueryDTO;
|
||||
matchType: RuletypesMatchTypeDTO;
|
||||
op: RuletypesCompareOperatorDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
requireMinPoints?: boolean;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
requiredNumPoints?: number;
|
||||
seasonality?: RuletypesSeasonalityDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
selectedQueryName?: string;
|
||||
/**
|
||||
* @type number
|
||||
* @nullable true
|
||||
*/
|
||||
target?: number | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
targetUnit?: string;
|
||||
thresholds?: RuletypesRuleThresholdDataDTO;
|
||||
}
|
||||
|
||||
export type RuletypesRuleThresholdDataDTO = RuletypesThresholdBasicDTO & {
|
||||
kind: RuletypesThresholdKindDTO;
|
||||
spec: unknown;
|
||||
};
|
||||
|
||||
export enum RuletypesRuleTypeDTO {
|
||||
threshold_rule = 'threshold_rule',
|
||||
promql_rule = 'promql_rule',
|
||||
anomaly_rule = 'anomaly_rule',
|
||||
}
|
||||
export interface RuletypesScheduleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
endTime?: Date;
|
||||
recurrence?: RuletypesRecurrenceDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
startTime?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export enum RuletypesScheduleTypeDTO {
|
||||
hourly = 'hourly',
|
||||
daily = 'daily',
|
||||
weekly = 'weekly',
|
||||
monthly = 'monthly',
|
||||
}
|
||||
export enum RuletypesSeasonalityDTO {
|
||||
hourly = 'hourly',
|
||||
daily = 'daily',
|
||||
weekly = 'weekly',
|
||||
}
|
||||
export interface RuletypesThresholdBasicDTO {
|
||||
kind?: RuletypesThresholdKindDTO;
|
||||
spec?: RuletypesBasicRuleThresholdsDTO;
|
||||
}
|
||||
|
||||
export enum RuletypesThresholdKindDTO {
|
||||
basic = 'basic',
|
||||
}
|
||||
export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -5456,6 +5958,49 @@ export type DeleteAuthDomainPathParameters = {
|
||||
export type UpdateAuthDomainPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type ListDowntimeSchedulesParams = {
|
||||
/**
|
||||
* @type boolean
|
||||
* @nullable true
|
||||
* @description undefined
|
||||
*/
|
||||
active?: boolean | null;
|
||||
/**
|
||||
* @type boolean
|
||||
* @nullable true
|
||||
* @description undefined
|
||||
*/
|
||||
recurring?: boolean | null;
|
||||
};
|
||||
|
||||
export type ListDowntimeSchedules200 = {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
data: RuletypesGettablePlannedMaintenanceDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteDowntimeScheduleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetDowntimeScheduleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetDowntimeScheduleByID200 = {
|
||||
data: RuletypesGettablePlannedMaintenanceDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateDowntimeScheduleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type HandleExportRawDataPOSTParams = {
|
||||
/**
|
||||
* @enum csv,jsonl
|
||||
@@ -6253,6 +6798,50 @@ export type GetUsersByRoleID200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListRules200 = {
|
||||
data: RuletypesGettableRulesDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateRule201 = {
|
||||
data: RuletypesGettableRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteRuleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetRuleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetRuleByID200 = {
|
||||
data: RuletypesGettableRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type PatchRuleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type PatchRuleByID200 = {
|
||||
data: RuletypesGettableRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateRuleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetRuleHistoryFilterKeysPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
@@ -6523,6 +7112,14 @@ export type GetRuleHistoryTopContributors200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type TestRule200 = {
|
||||
data: RuletypesGettableTestRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetSessionContext200 = {
|
||||
data: AuthtypesSessionContextDTO;
|
||||
/**
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
height: 32px;
|
||||
padding: 10px 16px;
|
||||
background: var(--l2-background);
|
||||
color: var(--l2-foreground);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
@@ -65,10 +66,3 @@
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.auth-header-help-button {
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
}
|
||||
|
||||
&__button {
|
||||
background: var(--card);
|
||||
color: var(--accent-primary);
|
||||
background: var(--secondary-background);
|
||||
color: var(--secondary-foreground);
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -5503,20 +5503,21 @@
|
||||
resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224"
|
||||
integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==
|
||||
|
||||
"@signozhq/button@0.0.1":
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/button/-/button-0.0.1.tgz#7d3204454b0361bd3fdf91fa6604af01a481a9db"
|
||||
integrity sha512-k5WFpckNXzwcTS82jU+65M3V1KdriopBObB1ls7W2OU0RKof6Gf+/9uqDXnuu+Y4Cxn2cPo8+6MfiQbS02LHeg==
|
||||
"@signozhq/button@0.0.5":
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/button/-/button-0.0.5.tgz#e8220a6e9ed78552694f41700c277956f26232e1"
|
||||
integrity sha512-fgobypuXv2kWGDkqXZoEjcySPHELzI/X515cdcR1hx4N9rizzOglLtYEjGTLR13iQrzLwSsNX8xxsv0/iSCjQg==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
"@signozhq/icons" "^0.1.0"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
lucide-react "^0.445.0"
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/button@0.0.2", "@signozhq/button@^0.0.2":
|
||||
"@signozhq/button@^0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/button/-/button-0.0.2.tgz#c13edef1e735134b784a41f874b60a14bc16993f"
|
||||
integrity sha512-434/gbTykC00LrnzFPp7c33QPWZkf9n+8+SToLZFTB0rzcaS/xoB4b7QKhvk+8xLCj4zpw6BxfeRAL+gSoOUJw==
|
||||
@@ -5529,14 +5530,28 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/calendar@0.0.0":
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/calendar/-/calendar-0.0.0.tgz#93b2cec2586efee814df934f88a2193cec95bae9"
|
||||
integrity sha512-lm7tzPEhaHNjrksvi2GPGH4suEe6x2DQJ2dpku+JmKyLGB5rg9saSAosvrZVKhXLoZuSSjlBSkz+oHYEKIdHfA==
|
||||
"@signozhq/button@workspace:*":
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/button/-/button-0.0.3.tgz#ce6c722b24859198f8c18a708b9d09249b184e3e"
|
||||
integrity sha512-b0JGoP0AIoYep/ApQUOn9LiK8dUATfqikz7jAKe20dPc8SjwUtsLPOao9j6I+2W4qd0CZDffJADBVpF2SAIsPQ==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
"@signozhq/icons" "^0.1.0"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
lucide-react "^0.445.0"
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/calendar@0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/calendar/-/calendar-0.1.1.tgz#8fb6432c4397ad2e8204b0ce9fb1b7aaa231e4cc"
|
||||
integrity sha512-Mw5cVtqSI1F57YwG+ufuJKJs/KrkrTSeP6aVPcTvO3tHJ5H4aZ1oaeZtGzTEDMqUvusDlRdZPSHhklJiUg/ixQ==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.2.3"
|
||||
"@signozhq/button" "0.0.1"
|
||||
"@signozhq/button" "workspace:*"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
date-fns "^4.1.0"
|
||||
@@ -5545,13 +5560,14 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/callout@0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/callout/-/callout-0.0.2.tgz#131ca15f89a8ee6729fecc4d322f11359c02e5cf"
|
||||
integrity sha512-tmguHm+/JVRKjMElJOFyG7LJcdqCW1hHnFfp8ZkjQ+Gi7MfFt/r2foLZG2DNdOcfxSvhf2zhzr7D+epgvmbQ1A==
|
||||
"@signozhq/callout@0.0.4":
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/callout/-/callout-0.0.4.tgz#8d0c224cc4f64930a04bd0d075597358ae7ec1d8"
|
||||
integrity sha512-g1NXkzAkuMdmH+z58t9CSL4+MZdWB5zPLyOHKeJYnQk1JXSYpEGK8CzSz4ZtVb4OrMS7mDkWxnqMK0EHxwVOWA==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
"@signozhq/icons" "^0.1.0"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
lucide-react "^0.445.0"
|
||||
@@ -5559,10 +5575,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/checkbox@0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/checkbox/-/checkbox-0.0.2.tgz#d11fb5eff3927c540937e3bd24351bfc1fdef9ec"
|
||||
integrity sha512-odQdh839GaTy1kqC8yavUKrOYP5tiIppUIV7xGNyxs/KnLGDWLw3ZSdACRV1Z55CLddjQ6OWKiwyVV7t+sxEuw==
|
||||
"@signozhq/checkbox@0.0.4":
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/checkbox/-/checkbox-0.0.4.tgz#2cf83d7bfd4db4aaec815e5788061e3fe5f86483"
|
||||
integrity sha512-X93EqHEy06pfpGcEJkJpNrxvkZryJaA+M0NQ09oTb1wzFweGMw+fR1Hgjl8lMqQPkHtz3MCP820uLhxvOJhktg==
|
||||
dependencies:
|
||||
"@radix-ui/react-checkbox" "^1.2.3"
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
@@ -5573,10 +5589,24 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/combobox@0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/combobox/-/combobox-0.0.2.tgz#019cc6d619e4eb6d1061fdfa00d4bd99d6aa727f"
|
||||
integrity sha512-QnGCNJAHd55Wqblw0CLOEOJoLFx8dgP+q/9hXbN5qil72DjRzxBgb5DAkIkon0owCmlagDQknFiOygYnzVJS8g==
|
||||
"@signozhq/checkbox@workspace:*":
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/checkbox/-/checkbox-0.0.3.tgz#9ccf8bd3b118405b2407c71db780089de050e651"
|
||||
integrity sha512-x2uqV3GsLmnDz4Zd2oY9/sExSqTY9gw/tw5gVd9ZziSNWOhy9rBKI8xwVpaAYiaZZHH3NReEwASwjx7C4EKKiQ==
|
||||
dependencies:
|
||||
"@radix-ui/react-checkbox" "^1.2.3"
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
lucide-react "^0.445.0"
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/combobox@0.0.4":
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/combobox/-/combobox-0.0.4.tgz#7195416144ae881874e3744f7b71251fbacc4f3e"
|
||||
integrity sha512-8mXlpkZ6066+JE+9EXmuR47ky+tJLwZ1W/9qXa69x5F1r7zXxuWvNQe/GYiGetpxohV/jO6QQDN1WpCV9hNjiw==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-popover" "^1.1.2"
|
||||
@@ -5589,10 +5619,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/command@0.0.0":
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/command/-/command-0.0.0.tgz#bd1e1cac7346e862dd61df64b756302e89e1a322"
|
||||
integrity sha512-AwRYxZTi4o8SBOL4hmgcgbhCKXl2Qb/TUSLbSYEMFdiQSl5VYA8XZJv5fSYVMJkAIlOaHzFzR04XNEU7lZcBpw==
|
||||
"@signozhq/command@0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/command/-/command-0.0.2.tgz#9d72f0d8d0945773461350f8d311072b2b4f96c6"
|
||||
integrity sha512-MFMDtm6qC/aG84k+q9XVCkRR46kNQYVP0qP59Xg34gx5baD76iivu13rZLc27MEhOucHacP2UODRJ4uMGZt/Mw==
|
||||
dependencies:
|
||||
"@radix-ui/react-dialog" "^1.1.11"
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
@@ -5609,24 +5639,25 @@
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/design-tokens/-/design-tokens-2.1.4.tgz#f209da6fbd2ac97ab4434b71472f741009306550"
|
||||
integrity sha512-Ny7/VA5YGFFmZx58jMh7ATFyu7VePaJ4ySmj/DopP1hilmfdxQsKWnpqKaZJWRXrbNkc0gmq3cR7q7Z8nnN7ZQ==
|
||||
|
||||
"@signozhq/dialog@^0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/dialog/-/dialog-0.0.2.tgz#55bd8e693f76325fda9aabe3197350e0adc163c4"
|
||||
integrity sha512-YT5t3oZpGkAuWptTqhCgVtLjxsRQrEIrQHFoXpP9elM1+O4TS9WHr+07BLQutOVg6u9n9pCvW3OYf0SCETkDVQ==
|
||||
"@signozhq/dialog@0.0.4":
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/dialog/-/dialog-0.0.4.tgz#54f385aa7cc17c6281653437e448f81dee0190ff"
|
||||
integrity sha512-j/RPhx98sCTyfg1VlSxbHfLzG/cVKCdd1JZrVkfzs4WK1ijM7Scpl4MUnFxn0ShCFrJ8trbR6I5NioKUIojK2g==
|
||||
dependencies:
|
||||
"@radix-ui/react-dialog" "^1.1.11"
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
"@signozhq/checkbox" "workspace:*"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
lucide-react "^0.445.0"
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/drawer@0.0.4":
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/drawer/-/drawer-0.0.4.tgz#7c6e6779602113f55df8a55076e68b9cc13c7d79"
|
||||
integrity sha512-m/shStl5yVPjHjrhDAh3EeKqqTtMmZUBVlgJPUGgoNV3sFsuN6JNaaAtEJI8cQBWkbEEiHLWKVkL/vhbQ7YrUg==
|
||||
"@signozhq/drawer@0.0.6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/drawer/-/drawer-0.0.6.tgz#ea280d71af6bc665679c7da26e0703a7bf96aa0e"
|
||||
integrity sha512-xoDHdsVZuj4rHK5gTnM4px7E2rv/6Jgqm81uxs1CfheOQsEAnPuiq3Dpdrdsf6XxCeORwnpcGUR5PEJhnAsjmA==
|
||||
dependencies:
|
||||
"@radix-ui/react-dialog" "^1.1.11"
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
@@ -5647,23 +5678,24 @@
|
||||
"@commitlint/cli" "^17.6.7"
|
||||
"@commitlint/config-conventional" "^17.6.7"
|
||||
|
||||
"@signozhq/input@0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/input/-/input-0.0.2.tgz#b2fea8c0979a53984ebcd5e3c3c50b38082eb1b1"
|
||||
integrity sha512-Iti9GkvexSsULX1pQsN6FT6Gw96YWilts72wITZd5fzgZq1yKqaDtQl98/QNuyoS3I3WEh+hVF4EIeCCe7oRsQ==
|
||||
"@signozhq/input@0.0.4":
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/input/-/input-0.0.4.tgz#f07dddb26ac4dda1cc8e07bfe605e8b34299955e"
|
||||
integrity sha512-S6tIcrkRZAsmwA80eAJCwZlgHLuioUHMTeszx4PRf/DUqVQ8cjIjmTfHDwzO0zIbMcVHpxqxVNpYArIqUJtgZQ==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
"@signozhq/button" "workspace:*"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
lucide-react "^0.445.0"
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/popover@0.0.0":
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/popover/-/popover-0.0.0.tgz#675baf1c18ca0180369b4df0700c24e2c55ad758"
|
||||
integrity sha512-XW0MhzxWzZNQWjVeb+BFjiOIbBbYCT+9MCUOIW8kiL0axFaaimnk0QPi1rk09u136MMGByI6fYuCJ5Qa07l1dA==
|
||||
"@signozhq/popover@0.1.2":
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/popover/-/popover-0.1.2.tgz#73b418be584e8a671ff4189ed47a540ed73bbde6"
|
||||
integrity sha512-6TVMVjWuO7XcKfFMrndcmDdg4JsGvRLe0SV43CRPNV6OkL5uFwUHFJZK2BU3v8S9xMoOf3LsdpfxPuCeK8QKCA==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-popover" "^1.1.15"
|
||||
@@ -5675,10 +5707,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/radio-group@0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/radio-group/-/radio-group-0.0.2.tgz#4b13567bfee2645226f2cf41f261bbb288e1be4b"
|
||||
integrity sha512-ahykmA5hPujOC964CFveMlQ12tWSyut2CUiFRqT1QxRkOLS2R44Qn2hh2psqJJ18JMX/24ZYCAIh9Bdd5XW+7g==
|
||||
"@signozhq/radio-group@0.0.4":
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/radio-group/-/radio-group-0.0.4.tgz#2fbd28bf48bb761063d642d3a169c3e0c098639d"
|
||||
integrity sha512-1t1idP+TsYV52GoQM/5KaxUQtA9/CAxAcVOvT6sBXAbyR12azUaR7GL4S0VxRRfOlA+pZ9bT9TqlfQT7LWUTDg==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-radio-group" "^1.3.4"
|
||||
@@ -5689,10 +5721,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/resizable@0.0.0":
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/resizable/-/resizable-0.0.0.tgz#a517818b9f9bcdaeafc55ae134be86522bc90e9f"
|
||||
integrity sha512-yAkJdMgTkh8kv42ZuabwTZguxalwYqIp4b44YdSrw6jRUSq9tscUBXVllNN79T71lPUtc5AV13uQ4Qm5AcfVbQ==
|
||||
"@signozhq/resizable@0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/resizable/-/resizable-0.0.2.tgz#76b9212c5f1b1e982013bec1b618e967a752e705"
|
||||
integrity sha512-xMdCDHOUDssPknFYRmwNTlQIy583wMpJSx6aHmjeYEcVfhDfDBrW+KEko/ODFoM8dOB0CpMtDjlLU14BchsNnA==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
@@ -5721,10 +5753,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/toggle-group@0.0.1":
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/toggle-group/-/toggle-group-0.0.1.tgz#c82ff1da34e77b24da53c2d595ad6b4a0d1b1de4"
|
||||
integrity sha512-871bQayL5MaqsuNOFHKexidu9W2Hlg1y4xmH8C5mGmlfZ4bd0ovJ9OweQrM6Puys3jeMwi69xmJuesYCfKQc1g==
|
||||
"@signozhq/toggle-group@0.0.3":
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/toggle-group/-/toggle-group-0.0.3.tgz#6a38312b46198c555ca4c287d4818135a4a9dabe"
|
||||
integrity sha512-qWoxC9jxW/otq1AdD/9ATGEJi4KyTfQSY8BSXlqu+4z4iEVjW4eeWSiyaoHcnB9UaypHDMFDMR+gPkyavtMY2Q==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
|
||||
360
pkg/alertmanager/alertmanagertemplate/alertmanagertemplate.go
Normal file
360
pkg/alertmanager/alertmanagertemplate/alertmanagertemplate.go
Normal file
@@ -0,0 +1,360 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/prometheus/alertmanager/notify"
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
// Templater expands user-authored title and body templates against a group
|
||||
// of alerts and returns channel-ready strings along with the aggregate data
|
||||
// a caller might reuse (e.g. to render an email layout around the body).
|
||||
type Templater interface {
|
||||
Expand(ctx context.Context, req alertmanagertypes.ExpandRequest, alerts []*types.Alert) (*alertmanagertypes.ExpandResult, error)
|
||||
}
|
||||
|
||||
type templater struct {
|
||||
tmpl *template.Template
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// New returns a Templater bound to the given Prometheus alertmanager
|
||||
// template and logger.
|
||||
func New(tmpl *template.Template, logger *slog.Logger) Templater {
|
||||
return &templater{tmpl: tmpl, logger: logger}
|
||||
}
|
||||
|
||||
func (at *templater) Expand(
|
||||
ctx context.Context,
|
||||
req alertmanagertypes.ExpandRequest,
|
||||
alerts []*types.Alert,
|
||||
) (*alertmanagertypes.ExpandResult, error) {
|
||||
ntd := at.buildNotificationTemplateData(ctx, alerts)
|
||||
missingVars := make(map[string]bool)
|
||||
|
||||
title, titleMissingVars, err := at.expandTitle(req.TitleTemplate, ntd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// if title template results in empty string, use default template
|
||||
// this happens for rules where custom title annotation was not set
|
||||
if title == "" && req.DefaultTitleTemplate != "" {
|
||||
title, err = at.expandDefaultTemplate(ctx, req.DefaultTitleTemplate, alerts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
mergeMissingVars(missingVars, titleMissingVars)
|
||||
}
|
||||
|
||||
isDefaultBody := false
|
||||
body, bodyMissingVars, err := at.expandBody(req.BodyTemplate, ntd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// if body template results in nil, use default template
|
||||
// this happens for rules where custom body annotation was not set
|
||||
if body == nil {
|
||||
isDefaultBody = true
|
||||
defaultBody, err := at.expandDefaultTemplate(ctx, req.DefaultBodyTemplate, alerts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = []string{defaultBody} // default template combines all alerts message into a single body
|
||||
} else {
|
||||
mergeMissingVars(missingVars, bodyMissingVars)
|
||||
}
|
||||
|
||||
// convert the internal map to a sorted slice for returning missing variables
|
||||
missingVarsList := make([]string, 0, len(missingVars))
|
||||
for k := range missingVars {
|
||||
missingVarsList = append(missingVarsList, k)
|
||||
}
|
||||
sort.Strings(missingVarsList)
|
||||
|
||||
return &alertmanagertypes.ExpandResult{
|
||||
Title: title,
|
||||
Body: body,
|
||||
MissingVars: missingVarsList,
|
||||
IsDefaultBody: isDefaultBody,
|
||||
NotificationData: ntd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// expandDefaultTemplate uses go-template to expand the default template.
|
||||
func (at *templater) expandDefaultTemplate(
|
||||
ctx context.Context,
|
||||
tmplStr string,
|
||||
alerts []*types.Alert,
|
||||
) (string, error) {
|
||||
// if even the default template is empty, return empty string
|
||||
// this is possible if user added channel with blank template
|
||||
if tmplStr == "" {
|
||||
at.logger.WarnContext(ctx, "default template is empty")
|
||||
return "", nil
|
||||
}
|
||||
data := notify.GetTemplateData(ctx, at.tmpl, alerts, at.logger)
|
||||
result, err := at.tmpl.ExecuteTextString(tmplStr, data)
|
||||
if err != nil {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to execute default template: %s", err.Error())
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// mergeMissingVars adds all keys from src into dst.
|
||||
func mergeMissingVars(dst, src map[string]bool) {
|
||||
for k := range src {
|
||||
dst[k] = true
|
||||
}
|
||||
}
|
||||
|
||||
// expandTitle expands the title template. Returns empty string if the template is empty.
|
||||
func (at *templater) expandTitle(
|
||||
titleTemplate string,
|
||||
ntd *alertmanagertypes.NotificationTemplateData,
|
||||
) (string, map[string]bool, error) {
|
||||
if titleTemplate == "" {
|
||||
return "", nil, nil
|
||||
}
|
||||
processRes, err := preProcessTemplateAndData(titleTemplate, ntd)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
result, err := at.tmpl.ExecuteTextString(processRes.Template, processRes.Data)
|
||||
if err != nil {
|
||||
return "", nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to execute custom title template: %s", err.Error())
|
||||
}
|
||||
return strings.TrimSpace(result), processRes.UnknownVars, nil
|
||||
}
|
||||
|
||||
// expandBody expands the body template for each individual alert. Returns nil if the template is empty.
|
||||
func (at *templater) expandBody(
|
||||
bodyTemplate string,
|
||||
ntd *alertmanagertypes.NotificationTemplateData,
|
||||
) ([]string, map[string]bool, error) {
|
||||
if bodyTemplate == "" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
var sb []string
|
||||
missingVars := make(map[string]bool)
|
||||
for i := range ntd.Alerts {
|
||||
processRes, err := preProcessTemplateAndData(bodyTemplate, &ntd.Alerts[i])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
part, err := at.tmpl.ExecuteTextString(processRes.Template, processRes.Data)
|
||||
if err != nil {
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to execute custom body template: %s", err.Error())
|
||||
}
|
||||
// add unknown variables and templated text to the result
|
||||
for k := range processRes.UnknownVars {
|
||||
missingVars[k] = true
|
||||
}
|
||||
if strings.TrimSpace(part) != "" {
|
||||
sb = append(sb, strings.TrimSpace(part))
|
||||
}
|
||||
}
|
||||
return sb, missingVars, nil
|
||||
}
|
||||
|
||||
// buildNotificationTemplateData creates the NotificationTemplateData using
|
||||
// info from context and the raw alerts.
|
||||
func (at *templater) buildNotificationTemplateData(
|
||||
ctx context.Context,
|
||||
alerts []*types.Alert,
|
||||
) *alertmanagertypes.NotificationTemplateData {
|
||||
// extract the required data from the context
|
||||
receiver, ok := notify.ReceiverName(ctx)
|
||||
if !ok {
|
||||
at.logger.WarnContext(ctx, "missing receiver name in context")
|
||||
}
|
||||
|
||||
groupLabels, ok := notify.GroupLabels(ctx)
|
||||
if !ok {
|
||||
at.logger.WarnContext(ctx, "missing group labels in context")
|
||||
}
|
||||
|
||||
// extract the external URL from the template
|
||||
externalURL := ""
|
||||
if at.tmpl.ExternalURL != nil {
|
||||
externalURL = at.tmpl.ExternalURL.String()
|
||||
}
|
||||
|
||||
commonAnnotations := extractCommonKV(alerts, func(a *types.Alert) model.LabelSet { return a.Annotations })
|
||||
commonLabels := extractCommonKV(alerts, func(a *types.Alert) model.LabelSet { return a.Labels })
|
||||
|
||||
// aggregate labels and annotations from all alerts
|
||||
labels := aggregateKV(alerts, func(a *types.Alert) model.LabelSet { return a.Labels })
|
||||
annotations := aggregateKV(alerts, func(a *types.Alert) model.LabelSet { return a.Annotations })
|
||||
|
||||
// Strip private annotations from surfaces visible to templates or
|
||||
// notifications; the structured fields on AlertInfo/RuleInfo already hold
|
||||
// anything a template needs from them.
|
||||
commonAnnotations = alertmanagertypes.FilterPublicAnnotations(commonAnnotations)
|
||||
annotations = alertmanagertypes.FilterPublicAnnotations(annotations)
|
||||
|
||||
// build the alert data slice
|
||||
alertDataSlice := make([]alertmanagertypes.AlertData, 0, len(alerts))
|
||||
for _, a := range alerts {
|
||||
ad := buildAlertData(a, receiver)
|
||||
alertDataSlice = append(alertDataSlice, ad)
|
||||
}
|
||||
|
||||
// count the number of firing and resolved alerts
|
||||
var firing, resolved int
|
||||
for _, ad := range alertDataSlice {
|
||||
if ad.Alert.IsFiring {
|
||||
firing++
|
||||
} else if ad.Alert.IsResolved {
|
||||
resolved++
|
||||
}
|
||||
}
|
||||
|
||||
// build the group labels
|
||||
gl := make(template.KV, len(groupLabels))
|
||||
for k, v := range groupLabels {
|
||||
gl[string(k)] = string(v)
|
||||
}
|
||||
|
||||
// build the notification template data
|
||||
return &alertmanagertypes.NotificationTemplateData{
|
||||
Alert: alertmanagertypes.NotificationAlert{
|
||||
Receiver: receiver,
|
||||
Status: string(types.Alerts(alerts...).Status()),
|
||||
TotalFiring: firing,
|
||||
TotalResolved: resolved,
|
||||
},
|
||||
Rule: buildRuleInfo(commonLabels, commonAnnotations),
|
||||
GroupLabels: gl,
|
||||
CommonLabels: commonLabels,
|
||||
CommonAnnotations: commonAnnotations,
|
||||
ExternalURL: externalURL,
|
||||
Labels: labels,
|
||||
Annotations: annotations,
|
||||
Alerts: alertDataSlice,
|
||||
}
|
||||
}
|
||||
|
||||
// buildAlertData converts a single *types.Alert into an AlertData.
|
||||
func buildAlertData(a *types.Alert, receiver string) alertmanagertypes.AlertData {
|
||||
labels := make(template.KV, len(a.Labels))
|
||||
for k, v := range a.Labels {
|
||||
labels[string(k)] = string(v)
|
||||
}
|
||||
|
||||
annotations := make(template.KV, len(a.Annotations))
|
||||
for k, v := range a.Annotations {
|
||||
annotations[string(k)] = string(v)
|
||||
}
|
||||
|
||||
return alertmanagertypes.AlertData{
|
||||
Alert: alertmanagertypes.AlertInfo{
|
||||
Status: string(a.Status()),
|
||||
Receiver: receiver,
|
||||
Value: annotations[ruletypes.AnnotationValue],
|
||||
StartsAt: a.StartsAt,
|
||||
EndsAt: a.EndsAt,
|
||||
GeneratorURL: a.GeneratorURL,
|
||||
Fingerprint: a.Fingerprint().String(),
|
||||
IsFiring: a.Status() == model.AlertFiring,
|
||||
IsResolved: a.Status() == model.AlertResolved,
|
||||
IsMissingData: labels[ruletypes.LabelNoData] == "true",
|
||||
IsRecovering: labels[ruletypes.LabelIsRecovering] == "true",
|
||||
},
|
||||
Rule: buildRuleInfo(labels, annotations),
|
||||
Log: alertmanagertypes.LinkInfo{URL: annotations[ruletypes.AnnotationRelatedLogs]},
|
||||
Trace: alertmanagertypes.LinkInfo{URL: annotations[ruletypes.AnnotationRelatedTraces]},
|
||||
Labels: labels,
|
||||
// Strip private annotations once the structured fields above have
|
||||
// been populated from the raw map.
|
||||
Annotations: alertmanagertypes.FilterPublicAnnotations(annotations),
|
||||
}
|
||||
}
|
||||
|
||||
// buildRuleInfo extracts the rule metadata from the well-known labels and
|
||||
// annotations that the rule manager attaches to every emitted alert.
|
||||
func buildRuleInfo(labels, annotations template.KV) alertmanagertypes.RuleInfo {
|
||||
return alertmanagertypes.RuleInfo{
|
||||
Name: labels[ruletypes.LabelAlertName],
|
||||
ID: labels[ruletypes.LabelRuleID],
|
||||
URL: labels[ruletypes.LabelRuleSource],
|
||||
Severity: labels[ruletypes.LabelSeverityName],
|
||||
MatchType: annotations[ruletypes.AnnotationMatchType],
|
||||
Threshold: alertmanagertypes.Threshold{
|
||||
Value: annotations[ruletypes.AnnotationThresholdValue],
|
||||
Op: annotations[ruletypes.AnnotationCompareOp],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// maxAggregatedValues caps the number of distinct label/annotation values
|
||||
// joined together when summarising across alerts. Beyond this, extras are
|
||||
// dropped rather than concatenated.
|
||||
const maxAggregatedValues = 5
|
||||
|
||||
// aggregateKV merges label or annotation sets from a group of alerts into a
|
||||
// single KV. Per key, up to maxAggregatedValues distinct values are kept (in
|
||||
// order of first appearance) and joined with ", ". A lossy summary used for
|
||||
// grouped-notification display, not a true union.
|
||||
func aggregateKV(alerts []*types.Alert, extractFn func(*types.Alert) model.LabelSet) template.KV {
|
||||
valuesPerKey := make(map[string][]string)
|
||||
seenValues := make(map[string]map[string]bool)
|
||||
|
||||
for _, alert := range alerts {
|
||||
for k, v := range extractFn(alert) {
|
||||
key := string(k)
|
||||
value := string(v)
|
||||
|
||||
if seenValues[key] == nil {
|
||||
seenValues[key] = make(map[string]bool)
|
||||
}
|
||||
if !seenValues[key][value] && len(valuesPerKey[key]) < maxAggregatedValues {
|
||||
seenValues[key][value] = true
|
||||
valuesPerKey[key] = append(valuesPerKey[key], value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(template.KV, len(valuesPerKey))
|
||||
for key, values := range valuesPerKey {
|
||||
result[key] = strings.Join(values, ", ")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// extractCommonKV returns the intersection of label or annotation pairs
|
||||
// across all alerts. A pair is included only if every alert carries the same
|
||||
// key with the same value.
|
||||
func extractCommonKV(alerts []*types.Alert, extractFn func(*types.Alert) model.LabelSet) template.KV {
|
||||
if len(alerts) == 0 {
|
||||
return template.KV{}
|
||||
}
|
||||
|
||||
common := make(template.KV, len(extractFn(alerts[0])))
|
||||
for k, v := range extractFn(alerts[0]) {
|
||||
common[string(k)] = string(v)
|
||||
}
|
||||
|
||||
for _, a := range alerts[1:] {
|
||||
kv := extractFn(a)
|
||||
for k := range common {
|
||||
if string(kv[model.LabelName(k)]) != common[k] {
|
||||
delete(common, k)
|
||||
}
|
||||
}
|
||||
if len(common) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return common
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
test "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/alertmanagernotifytest"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/prometheus/alertmanager/notify"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
)
|
||||
|
||||
// testSetup returns an AlertTemplater and a context pre-populated with group key,
|
||||
// receiver name, and group labels for use in tests.
|
||||
func testSetup(t *testing.T) (Templater, context.Context) {
|
||||
t.Helper()
|
||||
tmpl := test.CreateTmpl(t)
|
||||
ctx := context.Background()
|
||||
ctx = notify.WithGroupKey(ctx, "test-group")
|
||||
ctx = notify.WithReceiverName(ctx, "slack")
|
||||
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": "TestAlert", "severity": "critical"})
|
||||
logger := slog.New(slog.DiscardHandler)
|
||||
return New(tmpl, logger), ctx
|
||||
}
|
||||
|
||||
func createAlert(labels, annotations map[string]string, isFiring bool) *types.Alert {
|
||||
ls := model.LabelSet{}
|
||||
for k, v := range labels {
|
||||
ls[model.LabelName(k)] = model.LabelValue(v)
|
||||
}
|
||||
ann := model.LabelSet{}
|
||||
for k, v := range annotations {
|
||||
ann[model.LabelName(k)] = model.LabelValue(v)
|
||||
}
|
||||
startsAt := time.Now()
|
||||
var endsAt time.Time
|
||||
if isFiring {
|
||||
endsAt = startsAt.Add(time.Hour)
|
||||
} else {
|
||||
startsAt = startsAt.Add(-2 * time.Hour)
|
||||
endsAt = startsAt.Add(-time.Hour)
|
||||
}
|
||||
return &types.Alert{Alert: model.Alert{Labels: ls, Annotations: ann, StartsAt: startsAt, EndsAt: endsAt}}
|
||||
}
|
||||
|
||||
func TestExpandTemplates(t *testing.T) {
|
||||
at, ctx := testSetup(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
alerts []*types.Alert
|
||||
input alertmanagertypes.ExpandRequest
|
||||
wantTitle string
|
||||
wantBody []string
|
||||
wantMissingVars []string
|
||||
errorContains string
|
||||
wantIsDefaultBody bool
|
||||
}{
|
||||
{
|
||||
// High request throughput on a service — service is a custom label.
|
||||
// $labels.service extracts the label value; $annotations.description pulls the annotation.
|
||||
name: "new template: high request throughput for a service",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(
|
||||
map[string]string{
|
||||
ruletypes.LabelAlertName: "HighRequestThroughput",
|
||||
ruletypes.LabelSeverityName: "warning",
|
||||
"service.name": "payment-service",
|
||||
},
|
||||
map[string]string{"description": "Request rate exceeded 10k/s"},
|
||||
true,
|
||||
),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: "High request throughput for $service.name",
|
||||
BodyTemplate: `The service $service.name is getting high request. Please investigate.
|
||||
Severity: $rule.severity
|
||||
Status: $alert.status
|
||||
Service: $service.name
|
||||
Description: $description`,
|
||||
},
|
||||
wantTitle: "High request throughput for payment-service",
|
||||
wantBody: []string{`The service payment-service is getting high request. Please investigate.
|
||||
Severity: warning
|
||||
Status: firing
|
||||
Service: payment-service
|
||||
Description: Request rate exceeded 10k/s`},
|
||||
wantIsDefaultBody: false,
|
||||
},
|
||||
{
|
||||
// Disk usage alert using old Go template syntax throughout.
|
||||
// No custom templates — both title and body use the default fallback path.
|
||||
name: "old template: disk usage high on database host",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(
|
||||
map[string]string{ruletypes.LabelAlertName: "DiskUsageHigh",
|
||||
ruletypes.LabelSeverityName: "critical",
|
||||
"instance": "db-primary-01",
|
||||
},
|
||||
map[string]string{
|
||||
"summary": "Disk usage high on database host",
|
||||
"description": "Disk usage is high on the database host",
|
||||
"related_logs": "https://logs.example.com/search?q=DiskUsageHigh",
|
||||
"related_traces": "https://traces.example.com/search?q=DiskUsageHigh",
|
||||
},
|
||||
true,
|
||||
),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
DefaultTitleTemplate: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
|
||||
{{- if gt (len .CommonLabels) (len .GroupLabels) -}}
|
||||
{{" "}}(
|
||||
{{- with .CommonLabels.Remove .GroupLabels.Names }}
|
||||
{{- range $index, $label := .SortedPairs -}}
|
||||
{{ if $index }}, {{ end }}
|
||||
{{- $label.Name }}="{{ $label.Value -}}"
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
)
|
||||
{{- end }}`,
|
||||
DefaultBodyTemplate: `{{ range .Alerts -}}
|
||||
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
|
||||
|
||||
*Summary:* {{ .Annotations.summary }}
|
||||
*Description:* {{ .Annotations.description }}
|
||||
*RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}}
|
||||
*RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end}}
|
||||
|
||||
*Details:*
|
||||
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
|
||||
{{ end }}
|
||||
{{ end }}`,
|
||||
},
|
||||
wantTitle: "[FIRING:1] DiskUsageHigh for (instance=\"db-primary-01\")",
|
||||
// Written with explicit \n so trailing whitespace inside the body
|
||||
// (emitted by the un-trimmed "{{ end }}" in the default template)
|
||||
// survives format-on-save.
|
||||
wantBody: []string{"*Alert:* DiskUsageHigh - critical\n" +
|
||||
"\n" +
|
||||
" *Summary:* Disk usage high on database host\n" +
|
||||
" *Description:* Disk usage is high on the database host\n" +
|
||||
" *RelatedLogs:* View in <https://logs.example.com/search?q=DiskUsageHigh|logs explorer>\n" +
|
||||
" *RelatedTraces:* View in <https://traces.example.com/search?q=DiskUsageHigh|traces explorer>\n" +
|
||||
"\n" +
|
||||
" *Details:*\n" +
|
||||
" • *alertname:* DiskUsageHigh\n" +
|
||||
" • *instance:* db-primary-01\n" +
|
||||
" • *severity:* critical\n" +
|
||||
" \n" +
|
||||
" "},
|
||||
wantIsDefaultBody: true,
|
||||
},
|
||||
{
|
||||
// Pod crash loop on multiple pods — body is expanded once per alert
|
||||
// and joined with "\n\n", with the pod name pulled from labels.
|
||||
name: "new template: pod crash loop on multiple pods, body per-alert",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrashLoop", "pod": "api-worker-1"}, nil, true),
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrashLoop", "pod": "api-worker-2"}, nil, true),
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrashLoop", "pod": "api-worker-3"}, nil, true),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: "$rule.name: $alert.total_firing pods affected",
|
||||
BodyTemplate: "$labels.pod is crash looping",
|
||||
},
|
||||
wantTitle: "PodCrashLoop: 3 pods affected",
|
||||
wantBody: []string{"api-worker-1 is crash looping", "api-worker-2 is crash looping", "api-worker-3 is crash looping"},
|
||||
wantIsDefaultBody: false,
|
||||
},
|
||||
{
|
||||
// Incident partially resolved — one service still down, one recovered.
|
||||
// Title shows the aggregate counts; body shows per-service status.
|
||||
name: "new template: service degradation with mixed firing and resolved alerts",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "ServiceDown", "service": "auth-service"}, nil, true),
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "ServiceDown", "service": "payment-service"}, nil, false),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: "$alert.total_firing firing, $alert.total_resolved resolved",
|
||||
BodyTemplate: "$labels.service ($alert.status)",
|
||||
},
|
||||
wantTitle: "1 firing, 1 resolved",
|
||||
wantBody: []string{"auth-service (firing)", "payment-service (resolved)"},
|
||||
wantIsDefaultBody: false,
|
||||
},
|
||||
{
|
||||
// $environment is not a known AlertData or NotificationTemplateData field,
|
||||
// so it lands in MissingVars and renders as "<no value>" in the output.
|
||||
name: "missing vars: unknown $environment variable in title",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical"}, nil, true),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: "[$environment] $rule.name",
|
||||
},
|
||||
wantTitle: "[<no value>] HighCPU",
|
||||
wantMissingVars: []string{"environment"},
|
||||
wantIsDefaultBody: true,
|
||||
},
|
||||
{
|
||||
// $runbook_url is not a known field — someone tried to embed a runbook link
|
||||
// directly as a variable instead of via annotations.
|
||||
name: "missing vars: unknown $runbook_url variable in body",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "PodOOMKilled", ruletypes.LabelSeverityName: "warning"}, nil, true),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
BodyTemplate: "$rule.name: see runbook at $runbook_url",
|
||||
},
|
||||
wantBody: []string{"PodOOMKilled: see runbook at <no value>"},
|
||||
wantMissingVars: []string{"runbook_url"},
|
||||
},
|
||||
{
|
||||
// Both title and body use unknown variables; MissingVars is the union of both.
|
||||
name: "missing vars: unknown variables in both title and body",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "HighMemory", ruletypes.LabelSeverityName: "critical"}, nil, true),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: "[$environment] $rule.name and [{{ $service }}]",
|
||||
BodyTemplate: "$rule.name: see runbook at $runbook_url",
|
||||
},
|
||||
wantTitle: "[<no value>] HighMemory and [<no value>]",
|
||||
wantBody: []string{"HighMemory: see runbook at <no value>"},
|
||||
wantMissingVars: []string{"environment", "runbook_url", "service"},
|
||||
},
|
||||
{
|
||||
// Custom title template that expands to only whitespace triggers the fallback,
|
||||
// so the default title template is used instead.
|
||||
name: "fallback: whitespace-only custom title falls back to default",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical"}, nil, false),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: " ",
|
||||
DefaultTitleTemplate: "{{ .CommonLabels.alertname }} ({{ .Status | toUpper }})",
|
||||
DefaultBodyTemplate: "Runbook: https://runbook.example.com",
|
||||
},
|
||||
wantTitle: "HighCPU (RESOLVED)",
|
||||
wantBody: []string{"Runbook: https://runbook.example.com"},
|
||||
wantIsDefaultBody: true,
|
||||
},
|
||||
{
|
||||
name: "using non-existing function in template",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical"}, nil, true),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: "$rule.name ({{$severity | toUpperAndTrim}}) for $alertname",
|
||||
},
|
||||
errorContains: "function \"toUpperAndTrim\" not defined",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := at.Expand(ctx, tc.input, tc.alerts)
|
||||
if tc.errorContains != "" {
|
||||
require.ErrorContains(t, err, tc.errorContains)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
if tc.wantTitle != "" {
|
||||
require.Equal(t, tc.wantTitle, got.Title)
|
||||
}
|
||||
if tc.wantBody != nil {
|
||||
require.Equal(t, tc.wantBody, got.Body)
|
||||
}
|
||||
require.Equal(t, tc.wantIsDefaultBody, got.IsDefaultBody)
|
||||
|
||||
if len(tc.wantMissingVars) == 0 {
|
||||
require.Empty(t, got.MissingVars)
|
||||
} else {
|
||||
sort.Strings(tc.wantMissingVars)
|
||||
require.Equal(t, tc.wantMissingVars, got.MissingVars)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
33
pkg/alertmanager/alertmanagertemplate/annotations.go
Normal file
33
pkg/alertmanager/alertmanagertemplate/annotations.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
)
|
||||
|
||||
// ExtractTemplatesFromAnnotations pulls the user-authored title and body
|
||||
// templates off the well-known annotation keys attached by the rule manager.
|
||||
// A template is returned only if every alert in the group carries the same
|
||||
// value under that key; otherwise the empty string is returned for that slot
|
||||
// (which causes Expand to fall back to the channel default).
|
||||
func ExtractTemplatesFromAnnotations(alerts []*types.Alert) (titleTemplate, bodyTemplate string) {
|
||||
if len(alerts) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
title := string(alerts[0].Annotations[ruletypes.AnnotationTitleTemplate])
|
||||
body := string(alerts[0].Annotations[ruletypes.AnnotationBodyTemplate])
|
||||
|
||||
for _, a := range alerts[1:] {
|
||||
if title != "" && string(a.Annotations[ruletypes.AnnotationTitleTemplate]) != title {
|
||||
title = ""
|
||||
}
|
||||
if body != "" && string(a.Annotations[ruletypes.AnnotationBodyTemplate]) != body {
|
||||
body = ""
|
||||
}
|
||||
if title == "" && body == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return title, body
|
||||
}
|
||||
153
pkg/alertmanager/alertmanagertemplate/kvaggregate_test.go
Normal file
153
pkg/alertmanager/alertmanagertemplate/kvaggregate_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAggregateKV(t *testing.T) {
|
||||
extractLabels := func(a *types.Alert) model.LabelSet { return a.Labels }
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
alerts []*types.Alert
|
||||
extractFn func(*types.Alert) model.LabelSet
|
||||
expected template.KV
|
||||
}{
|
||||
{
|
||||
name: "empty alerts slice",
|
||||
alerts: []*types.Alert{},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{},
|
||||
},
|
||||
{
|
||||
name: "single alert",
|
||||
alerts: []*types.Alert{
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{
|
||||
"env": "production",
|
||||
"service": "backend",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{
|
||||
"env": "production",
|
||||
"service": "backend",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "varying values with duplicates deduped",
|
||||
alerts: []*types.Alert{
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "production", "service": "backend"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "production", "service": "api"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "production", "service": "frontend"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "production", "service": "api"}}},
|
||||
},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{
|
||||
"env": "production",
|
||||
"service": "backend, api, frontend",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "more than 5 unique values truncates to 5",
|
||||
alerts: []*types.Alert{
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc1"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc2"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc3"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc4"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc5"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc6"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc7"}}},
|
||||
},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{
|
||||
"service": "svc1, svc2, svc3, svc4, svc5",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := aggregateKV(tc.alerts, tc.extractFn)
|
||||
require.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractCommonKV(t *testing.T) {
|
||||
extractLabels := func(a *types.Alert) model.LabelSet { return a.Labels }
|
||||
extractAnnotations := func(a *types.Alert) model.LabelSet { return a.Annotations }
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
alerts []*types.Alert
|
||||
extractFn func(*types.Alert) model.LabelSet
|
||||
expected template.KV
|
||||
}{
|
||||
{
|
||||
name: "empty alerts slice",
|
||||
alerts: []*types.Alert{},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{},
|
||||
},
|
||||
{
|
||||
name: "single alert returns all labels",
|
||||
alerts: []*types.Alert{
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "service": "api"}}},
|
||||
},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{"env": "prod", "service": "api"},
|
||||
},
|
||||
{
|
||||
name: "multiple alerts with fully common labels",
|
||||
alerts: []*types.Alert{
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "region": "us-east"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "region": "us-east"}}},
|
||||
},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{"env": "prod", "region": "us-east"},
|
||||
},
|
||||
{
|
||||
name: "multiple alerts with partially common labels",
|
||||
alerts: []*types.Alert{
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "service": "api"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "service": "worker"}}},
|
||||
},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{"env": "prod"},
|
||||
},
|
||||
{
|
||||
name: "multiple alerts with no common labels",
|
||||
alerts: []*types.Alert{
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "api"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "worker"}}},
|
||||
},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{},
|
||||
},
|
||||
{
|
||||
name: "annotations extract common annotations",
|
||||
alerts: []*types.Alert{
|
||||
{Alert: model.Alert{Annotations: model.LabelSet{"summary": "high cpu", "runbook": "http://x"}}},
|
||||
{Alert: model.Alert{Annotations: model.LabelSet{"summary": "high cpu", "runbook": "http://y"}}},
|
||||
},
|
||||
extractFn: extractAnnotations,
|
||||
expected: template.KV{"summary": "high cpu"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := extractCommonKV(tc.alerts, tc.extractFn)
|
||||
require.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
318
pkg/alertmanager/alertmanagertemplate/preprocessor.go
Normal file
318
pkg/alertmanager/alertmanagertemplate/preprocessor.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
)
|
||||
|
||||
// fieldPath is a dotted mapstructure path into the templating data map,
|
||||
// e.g. "alert.is_firing" or "rule.threshold.value".
|
||||
type fieldPath string
|
||||
|
||||
// extractFieldMappings flattens the struct hierarchy into a list of dotted
|
||||
// mapstructure paths that user templates can reference. It emits:
|
||||
// - a leaf for every scalar field
|
||||
// - a leaf for every map field (labels, annotations)
|
||||
// - a mapping for each intermediate sub-struct itself, so {{ $alert := .alert }}
|
||||
// bindings let action blocks write {{ if $alert.is_firing }}
|
||||
//
|
||||
// Slices and interfaces are not surfaced. Pointer fields are dereferenced.
|
||||
func extractFieldMappings(data any) []fieldPath {
|
||||
val := reflect.ValueOf(data)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
if val.IsNil() {
|
||||
return nil
|
||||
}
|
||||
val = val.Elem()
|
||||
}
|
||||
if val.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
return collectFieldMappings(val, "")
|
||||
}
|
||||
|
||||
func collectFieldMappings(val reflect.Value, prefix string) []fieldPath {
|
||||
typ := val.Type()
|
||||
var paths []fieldPath
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
tag := field.Tag.Get("mapstructure")
|
||||
if tag == "" || tag == "-" {
|
||||
continue
|
||||
}
|
||||
name := strings.Split(tag, ",")[0]
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
key := name
|
||||
if prefix != "" {
|
||||
key = prefix + "." + name
|
||||
}
|
||||
|
||||
ft := field.Type
|
||||
if ft.Kind() == reflect.Ptr {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
|
||||
switch ft.Kind() {
|
||||
case reflect.Slice, reflect.Interface:
|
||||
continue
|
||||
}
|
||||
|
||||
// Recurse into sub-structs (time.Time treated as a leaf).
|
||||
if ft.Kind() == reflect.Struct && ft.String() != "time.Time" {
|
||||
paths = append(paths, fieldPath(key))
|
||||
fv := val.Field(i)
|
||||
if fv.Kind() == reflect.Ptr {
|
||||
if fv.IsNil() {
|
||||
continue
|
||||
}
|
||||
fv = fv.Elem()
|
||||
}
|
||||
paths = append(paths, collectFieldMappings(fv, key)...)
|
||||
continue
|
||||
}
|
||||
|
||||
paths = append(paths, fieldPath(key))
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// structRootSet returns the top-level mapstructure tag names whose field
|
||||
// type is a nested struct (excluding time.Time and map/slice/interface
|
||||
// fields). These are the paths the rewriter walks segment-by-segment; any
|
||||
// other dotted $-reference is treated as a flat key on the root map so that
|
||||
// flattened OTel-style label keys like "service.name" resolve naturally.
|
||||
func structRootSet(data any) map[string]bool {
|
||||
val := reflect.ValueOf(data)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
if val.IsNil() {
|
||||
return nil
|
||||
}
|
||||
val = val.Elem()
|
||||
}
|
||||
if val.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
roots := make(map[string]bool)
|
||||
typ := val.Type()
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
tag := field.Tag.Get("mapstructure")
|
||||
if tag == "" || tag == "-" {
|
||||
continue
|
||||
}
|
||||
name := strings.Split(tag, ",")[0]
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
ft := field.Type
|
||||
if ft.Kind() == reflect.Ptr {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
if ft.Kind() == reflect.Struct && ft.String() != "time.Time" {
|
||||
roots[name] = true
|
||||
}
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
// buildDataMap converts the typed data struct to the map[string]any that the
|
||||
// template engine indexes into. Each label and annotation is additionally
|
||||
// exposed at the root under its raw key, so $service.name resolves a flat
|
||||
// OTel-style label as a single-key index on the root. Struct-path keys
|
||||
// already present at the root take precedence on collisions.
|
||||
func buildDataMap(data any) (map[string]any, error) {
|
||||
var result map[string]any
|
||||
if err := mapstructure.Decode(data, &result); err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to build template data map")
|
||||
}
|
||||
|
||||
flatten := func(labels, annotations map[string]string) {
|
||||
for k, v := range labels {
|
||||
if _, ok := result[k]; !ok {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
for k, v := range annotations {
|
||||
if _, ok := result[k]; !ok {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch data := data.(type) {
|
||||
case *alertmanagertypes.NotificationTemplateData:
|
||||
flatten(data.Labels, data.Annotations)
|
||||
case *alertmanagertypes.AlertData:
|
||||
flatten(data.Labels, data.Annotations)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// renderPreamble serialises a map of binding name → RHS expression into
|
||||
// `{{ $name := expr }}` declarations. Dotted names are skipped: Go's
|
||||
// text/template parser rejects `{{ $a.b := ... }}`; dotted paths are resolved
|
||||
// at expansion time by the rewriter.
|
||||
func renderPreamble(bindings map[string]string) string {
|
||||
if len(bindings) == 0 {
|
||||
return ""
|
||||
}
|
||||
var sb strings.Builder
|
||||
for name, expr := range bindings {
|
||||
if strings.Contains(name, ".") {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&sb, `{{ $%s := %s }}`, name, expr)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// buildPreamble constructs the variable-definition preamble prepended to the
|
||||
// user template, covering:
|
||||
// - known root-level struct paths ({{ $alert := .alert }})
|
||||
// - "<no value>" stubs for $-refs whose first segment matches nothing, so
|
||||
// action blocks like {{ if $custom_note }} don't error at parse time
|
||||
//
|
||||
// The set of unmatched names is returned separately so callers (preview API)
|
||||
// can surface warnings.
|
||||
func buildPreamble(tmpl string, data any) (string, map[string]bool, error) {
|
||||
bindings := make(map[string]string)
|
||||
// knownFirstSegments tracks every valid first segment of a $-ref, since
|
||||
// extractUsedVariables only gives us first segments. A label key like
|
||||
// "service.name" contributes "service" here, so $service.name isn't
|
||||
// flagged as unknown even though "service" has no direct binding.
|
||||
knownFirstSegments := make(map[string]bool)
|
||||
|
||||
for _, p := range extractFieldMappings(data) {
|
||||
bindings[string(p)] = fmt.Sprintf(".%s", p)
|
||||
knownFirstSegments[firstSegment(string(p))] = true
|
||||
}
|
||||
// Labels/annotations are flattened into the root map by buildDataMap, so
|
||||
// a bare-accessible key (no dots) can be bound in the preamble — this is
|
||||
// what makes {{ if $severity }} or {{ $severity | toUpper }} work in
|
||||
// action blocks. Dotted label keys only contribute to knownFirstSegments:
|
||||
// their action-block use would be a syntax error anyway ($a.b is not a
|
||||
// valid Go template identifier).
|
||||
for k := range dataLabelsAndAnnotations(data) {
|
||||
knownFirstSegments[firstSegment(k)] = true
|
||||
if !strings.Contains(k, ".") {
|
||||
if _, ok := bindings[k]; !ok {
|
||||
bindings[k] = fmt.Sprintf(".%s", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
used, err := extractUsedVariables(tmpl)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
unknown := make(map[string]bool)
|
||||
for name := range used {
|
||||
if !knownFirstSegments[name] {
|
||||
unknown[name] = true
|
||||
bindings[name] = `"<no value>"`
|
||||
}
|
||||
}
|
||||
|
||||
return renderPreamble(bindings), unknown, nil
|
||||
}
|
||||
|
||||
// firstSegment returns the portion of a dotted path before the first dot,
|
||||
// or the whole string if there is no dot.
|
||||
func firstSegment(path string) string {
|
||||
if i := strings.IndexByte(path, '.'); i >= 0 {
|
||||
return path[:i]
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// dataLabelsAndAnnotations returns the union of label and annotation keys on
|
||||
// the given data struct (if it carries them). Used for first-segment
|
||||
// recognition of $-refs that point into flat OTel-style label keys.
|
||||
func dataLabelsAndAnnotations(data any) map[string]struct{} {
|
||||
keys := make(map[string]struct{})
|
||||
switch d := data.(type) {
|
||||
case *alertmanagertypes.NotificationTemplateData:
|
||||
for k := range d.Labels {
|
||||
keys[k] = struct{}{}
|
||||
}
|
||||
for k := range d.Annotations {
|
||||
keys[k] = struct{}{}
|
||||
}
|
||||
case *alertmanagertypes.AlertData:
|
||||
for k := range d.Labels {
|
||||
keys[k] = struct{}{}
|
||||
}
|
||||
for k := range d.Annotations {
|
||||
keys[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// processingResult is the rewritten template and its backing data map,
|
||||
// ready to be passed to Go text/template's Execute.
|
||||
type processingResult struct {
|
||||
// Template is the input template with $-refs rewritten to Go template
|
||||
// syntax and an identifier preamble prepended.
|
||||
Template string
|
||||
// Data is the flattened map the template indexes into.
|
||||
Data map[string]any
|
||||
// UnknownVars are $-refs in the input that had no matching data path.
|
||||
// They render as "<no value>" at execution; useful for preview warnings.
|
||||
UnknownVars map[string]bool
|
||||
}
|
||||
|
||||
// preProcessTemplateAndData prepares a user-authored template and its backing
|
||||
// struct for Go text/template execution.
|
||||
//
|
||||
// Input (with data *AlertData):
|
||||
// "$rule.name fired with value $alert.value"
|
||||
// Output:
|
||||
// "{{ $alert := .alert }}{{ $rule := .rule }}..."
|
||||
// "{{ index . \"rule\" \"name\" }} fired with value {{ index . \"alert\" \"value\" }}"
|
||||
func preProcessTemplateAndData(tmpl string, data any) (*processingResult, error) {
|
||||
unknownVars := make(map[string]bool)
|
||||
if tmpl == "" {
|
||||
result, err := buildDataMap(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &processingResult{Data: result, UnknownVars: unknownVars}, nil
|
||||
}
|
||||
|
||||
preamble, unknownVars, err := buildPreamble(tmpl, data)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to build template preamble")
|
||||
}
|
||||
|
||||
// Prepend the preamble so wrapDollarVariables can parse the AST without
|
||||
// "undefined variable" errors for $-refs inside action blocks.
|
||||
wrapped, err := wrapDollarVariables(preamble+tmpl, structRootSet(data))
|
||||
if err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to rewrite template")
|
||||
}
|
||||
|
||||
result, err := buildDataMap(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &processingResult{Template: wrapped, Data: result, UnknownVars: unknownVars}, nil
|
||||
}
|
||||
380
pkg/alertmanager/alertmanagertemplate/preprocessor_test.go
Normal file
380
pkg/alertmanager/alertmanagertemplate/preprocessor_test.go
Normal file
@@ -0,0 +1,380 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractFieldMappings(t *testing.T) {
|
||||
// Flat struct: mapstructure-tagged leaves only. Slices and interfaces are
|
||||
// dropped; maps (labels/annotations analogues) are kept as top-level leaves.
|
||||
type Flat struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Status string `mapstructure:"status"`
|
||||
UserCount int `mapstructure:"user_count"`
|
||||
IsActive bool `mapstructure:"is_active"`
|
||||
CreatedAt time.Time `mapstructure:"created_at"`
|
||||
Extra map[string]string `mapstructure:"extra"`
|
||||
Items []string `mapstructure:"items"` // slice skipped
|
||||
unexported string //nolint:unused // unexported skipped
|
||||
NoTag string // no mapstructure tag skipped
|
||||
SkippedTag string `mapstructure:"-"` // explicit skip
|
||||
}
|
||||
|
||||
// Nested struct: sub-struct paths are flattened into dotted mappings; the
|
||||
// parent path itself is also emitted so templates can bind `$alert := .alert`.
|
||||
type Inner struct {
|
||||
Value string `mapstructure:"value"`
|
||||
Op string `mapstructure:"op"`
|
||||
}
|
||||
type Outer struct {
|
||||
Alert struct {
|
||||
Status string `mapstructure:"status"`
|
||||
IsFiring bool `mapstructure:"is_firing"`
|
||||
} `mapstructure:"alert"`
|
||||
Rule struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Threshold Inner `mapstructure:"threshold"`
|
||||
} `mapstructure:"rule"`
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
data any
|
||||
expected []fieldPath
|
||||
}{
|
||||
{
|
||||
name: "flat struct surfaces only mapstructure-tagged scalars",
|
||||
data: Flat{},
|
||||
expected: []fieldPath{
|
||||
"name", "status", "user_count", "is_active", "created_at", "extra",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested struct emits parent and dotted leaf paths",
|
||||
data: Outer{},
|
||||
expected: []fieldPath{
|
||||
"alert",
|
||||
"alert.status",
|
||||
"alert.is_firing",
|
||||
"rule",
|
||||
"rule.name",
|
||||
"rule.threshold",
|
||||
"rule.threshold.value",
|
||||
"rule.threshold.op",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil data",
|
||||
data: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "non-struct type",
|
||||
data: "string",
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := extractFieldMappings(tc.data)
|
||||
require.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildVariableDefinitions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
tmpl string
|
||||
data any
|
||||
expectedVars []string // substrings that must appear in result
|
||||
forbiddenVars []string // substrings that must NOT appear (dotted identifiers)
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "empty template still emits struct bindings for title data",
|
||||
tmpl: "",
|
||||
data: &alertmanagertypes.NotificationTemplateData{Alert: alertmanagertypes.NotificationAlert{Receiver: "slack"}},
|
||||
expectedVars: []string{
|
||||
"{{ $alert := .alert }}",
|
||||
"{{ $rule := .rule }}",
|
||||
},
|
||||
// Dotted leaves are NOT emitted as preamble bindings — they're
|
||||
// reached via {{ index . "alert" "status" }} at expansion time.
|
||||
forbiddenVars: []string{
|
||||
"$alert.status",
|
||||
"$rule.threshold.value",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mix of known and unknown vars in alert body",
|
||||
tmpl: "$rule.name fired: $custom_label",
|
||||
data: &alertmanagertypes.AlertData{Rule: alertmanagertypes.RuleInfo{Name: "test"}, Alert: alertmanagertypes.AlertInfo{Status: "firing"}},
|
||||
expectedVars: []string{
|
||||
"{{ $alert := .alert }}",
|
||||
"{{ $rule := .rule }}",
|
||||
`{{ $custom_label := "<no value>" }}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "known dotted variables do not get flagged as unknown",
|
||||
tmpl: "$alert.is_firing and $rule.threshold.value",
|
||||
data: &alertmanagertypes.AlertData{},
|
||||
// $alert and $rule (first segments) are in mappings, so no unknown
|
||||
// stubs; the dotted leaves are resolved by WrapDollarVariables.
|
||||
expectedVars: []string{
|
||||
"{{ $alert := .alert }}",
|
||||
"{{ $rule := .rule }}",
|
||||
},
|
||||
forbiddenVars: []string{
|
||||
`"<no value>"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Label-derived $-refs aren't stubbed as unknown; their first
|
||||
// segment is marked known so {{ $severity := ... }} stubs don't
|
||||
// appear in the preamble. Resolution happens at expansion via the
|
||||
// root-level flattening performed in buildDataMap.
|
||||
name: "label first-segments suppress unknown-var stubs",
|
||||
tmpl: "$severity for $service $cloud.region.instance",
|
||||
data: &alertmanagertypes.NotificationTemplateData{Labels: template.KV{
|
||||
"severity": "critical",
|
||||
"service": "test",
|
||||
"cloud.region.instance": "ap-south-1",
|
||||
}},
|
||||
forbiddenVars: []string{
|
||||
`{{ $severity := "<no value>" }}`,
|
||||
`{{ $service := "<no value>" }}`,
|
||||
`{{ $cloud := "<no value>" }}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same rule holds for AlertData labels",
|
||||
tmpl: "$severity $service",
|
||||
data: &alertmanagertypes.AlertData{Labels: template.KV{
|
||||
"severity": "critical",
|
||||
"service": "test",
|
||||
}},
|
||||
forbiddenVars: []string{
|
||||
`{{ $severity := "<no value>" }}`,
|
||||
`{{ $service := "<no value>" }}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid template syntax returns error",
|
||||
tmpl: "{{invalid",
|
||||
data: &alertmanagertypes.NotificationTemplateData{},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, _, err := buildPreamble(tc.tmpl, tc.data)
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
for _, expected := range tc.expectedVars {
|
||||
require.Contains(t, result, expected, "expected preamble substring missing")
|
||||
}
|
||||
for _, forbidden := range tc.forbiddenVars {
|
||||
require.NotContains(t, result, forbidden, "unexpected preamble substring present")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreProcessTemplateAndData(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
tmpl string
|
||||
data any
|
||||
expectedTemplateContains []string
|
||||
expectedData map[string]any
|
||||
expectedUnknownVars map[string]bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "title template: struct-root walks and flat dotted label keys",
|
||||
tmpl: "[$alert.status] $rule.name (ID: $rule.id) firing=$alert.total_firing severity=$severity method=$http.request.method",
|
||||
data: &alertmanagertypes.NotificationTemplateData{
|
||||
Alert: alertmanagertypes.NotificationAlert{
|
||||
Receiver: "pagerduty",
|
||||
Status: "firing",
|
||||
TotalFiring: 3,
|
||||
},
|
||||
Rule: alertmanagertypes.RuleInfo{
|
||||
Name: "HighMemory",
|
||||
ID: "rule-123",
|
||||
},
|
||||
Labels: template.KV{
|
||||
"severity": "critical",
|
||||
"http.request.method": "GET",
|
||||
},
|
||||
},
|
||||
expectedTemplateContains: []string{
|
||||
"{{$alert := .alert}}",
|
||||
"{{$rule := .rule}}",
|
||||
`[{{ index . "alert" "status" }}] {{ index . "rule" "name" }} (ID: {{ index . "rule" "id" }})`,
|
||||
`firing={{ index . "alert" "total_firing" }} severity={{ .severity }}`,
|
||||
`method={{ index . "http.request.method" }}`,
|
||||
},
|
||||
expectedData: map[string]any{
|
||||
"alert": map[string]any{
|
||||
"receiver": "pagerduty",
|
||||
"status": "firing",
|
||||
"total_firing": 3,
|
||||
"total_resolved": 0,
|
||||
},
|
||||
"severity": "critical",
|
||||
"http.request.method": "GET",
|
||||
},
|
||||
expectedUnknownVars: map[string]bool{},
|
||||
},
|
||||
{
|
||||
name: "body template with nested threshold access and per-alert annotation",
|
||||
tmpl: "$rule.name: value $alert.value $rule.threshold.op $rule.threshold.value ($alert.status) desc=$description",
|
||||
data: &alertmanagertypes.AlertData{
|
||||
Alert: alertmanagertypes.AlertInfo{
|
||||
Status: "firing",
|
||||
Value: "85%",
|
||||
IsFiring: true,
|
||||
},
|
||||
Rule: alertmanagertypes.RuleInfo{
|
||||
Name: "DiskFull",
|
||||
ID: "disk-001",
|
||||
Severity: "warning",
|
||||
Threshold: alertmanagertypes.Threshold{Value: "80%", Op: ">"},
|
||||
},
|
||||
Annotations: template.KV{
|
||||
"description": "Disk full and cannot be written to",
|
||||
},
|
||||
},
|
||||
expectedTemplateContains: []string{
|
||||
"{{$alert := .alert}}",
|
||||
"{{$rule := .rule}}",
|
||||
// "description" is an annotation flattened to root; the preamble
|
||||
// now binds it off the root rather than via .annotations lookup.
|
||||
"{{$description := .description}}",
|
||||
`{{ index . "rule" "name" }}: value {{ index . "alert" "value" }} {{ index . "rule" "threshold" "op" }} {{ index . "rule" "threshold" "value" }}`,
|
||||
},
|
||||
expectedData: map[string]any{
|
||||
"description": "Disk full and cannot be written to",
|
||||
},
|
||||
expectedUnknownVars: map[string]bool{},
|
||||
},
|
||||
{
|
||||
// Struct roots reserve their first-segment namespace: a label
|
||||
// whose key starts with "alert." is shadowed by the Alert sub-map,
|
||||
// and must be accessed via the explicit $labels.* prefix.
|
||||
name: "label colliding with struct root is accessed via $labels.*",
|
||||
tmpl: "$alert.status via $labels.alert.custom",
|
||||
data: &alertmanagertypes.NotificationTemplateData{
|
||||
Alert: alertmanagertypes.NotificationAlert{Status: "firing"},
|
||||
Labels: template.KV{"alert.custom": "x"},
|
||||
},
|
||||
expectedTemplateContains: []string{
|
||||
`{{ index . "alert" "status" }}`,
|
||||
`{{ index .labels "alert.custom" }}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Same shadowing rule applies symmetrically to annotations.
|
||||
name: "annotation colliding with struct root is accessed via $annotations.*",
|
||||
tmpl: "$alert.status via $annotations.alert.meta",
|
||||
data: &alertmanagertypes.NotificationTemplateData{
|
||||
Alert: alertmanagertypes.NotificationAlert{Status: "firing"},
|
||||
Annotations: template.KV{"alert.meta": "x"},
|
||||
},
|
||||
expectedTemplateContains: []string{
|
||||
`{{ index . "alert" "status" }}`,
|
||||
`{{ index .annotations "alert.meta" }}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
// When a label and an annotation share a key, the label wins at the
|
||||
// root flattening layer. Users who want the annotation must address
|
||||
// it explicitly via $annotations.<key>.
|
||||
name: "label takes precedence over same-named annotation at root",
|
||||
tmpl: "flat=$env labels_only=$labels.env annotations_only=$annotations.env",
|
||||
data: &alertmanagertypes.NotificationTemplateData{
|
||||
Labels: template.KV{"env": "prod"},
|
||||
Annotations: template.KV{"env": "staging"},
|
||||
},
|
||||
expectedTemplateContains: []string{
|
||||
`flat={{ .env }}`,
|
||||
`labels_only={{ index .labels "env" }}`,
|
||||
`annotations_only={{ index .annotations "env" }}`,
|
||||
},
|
||||
expectedData: map[string]any{
|
||||
"env": "prod",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty template returns flattened data",
|
||||
tmpl: "",
|
||||
data: &alertmanagertypes.NotificationTemplateData{Alert: alertmanagertypes.NotificationAlert{Receiver: "slack"}},
|
||||
},
|
||||
{
|
||||
name: "invalid template syntax",
|
||||
tmpl: "{{invalid",
|
||||
data: &alertmanagertypes.NotificationTemplateData{},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "unknown dollar var in text renders empty",
|
||||
tmpl: "alert $custom_note fired",
|
||||
data: &alertmanagertypes.NotificationTemplateData{Rule: alertmanagertypes.RuleInfo{Name: "HighCPU"}},
|
||||
expectedTemplateContains: []string{
|
||||
`{{$custom_note := "<no value>"}}`,
|
||||
"alert {{ .custom_note }} fired",
|
||||
},
|
||||
expectedUnknownVars: map[string]bool{"custom_note": true},
|
||||
},
|
||||
{
|
||||
name: "unknown dollar var in action block renders empty",
|
||||
tmpl: "alert {{ $custom_note }} fired",
|
||||
data: &alertmanagertypes.NotificationTemplateData{Rule: alertmanagertypes.RuleInfo{Name: "HighCPU"}},
|
||||
expectedTemplateContains: []string{
|
||||
`{{$custom_note := "<no value>"}}`,
|
||||
`alert {{$custom_note}} fired`,
|
||||
},
|
||||
expectedUnknownVars: map[string]bool{"custom_note": true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := preProcessTemplateAndData(tc.tmpl, tc.data)
|
||||
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
if tc.tmpl == "" {
|
||||
require.Equal(t, "", result.Template)
|
||||
return
|
||||
}
|
||||
|
||||
for _, substr := range tc.expectedTemplateContains {
|
||||
require.Contains(t, result.Template, substr)
|
||||
}
|
||||
for k, v := range tc.expectedData {
|
||||
require.Equal(t, v, result.Data[k], "data[%q] mismatch", k)
|
||||
}
|
||||
if tc.expectedUnknownVars != nil {
|
||||
require.Equal(t, tc.expectedUnknownVars, result.UnknownVars)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
155
pkg/alertmanager/alertmanagertemplate/rewrite.go
Normal file
155
pkg/alertmanager/alertmanagertemplate/rewrite.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template/parse"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
// bareVariableRegex matches $-references including dotted paths (e.g. $alert.is_firing).
|
||||
var bareVariableRegex = regexp.MustCompile(`\$(\w+(?:\.\w+)*)`)
|
||||
|
||||
// bareVariableFirstSegRegex captures only the first segment of a $-reference.
|
||||
// $labels.severity yields "$labels"; $alert.is_firing yields "$alert".
|
||||
var bareVariableFirstSegRegex = regexp.MustCompile(`\$\w+`)
|
||||
|
||||
// wrapDollarVariables rewrites bare $-references in a template's plain-text
|
||||
// regions into Go text/template syntax. References inside `{{ ... }}` action
|
||||
// blocks are left untouched — they're already template syntax. structRoots
|
||||
// is the set of top-level mapstructure names whose values are nested structs
|
||||
// (e.g. "alert", "rule"): $-refs whose first segment is in this set are
|
||||
// walked segment-by-segment; everything else dotted is treated as a flat key
|
||||
// on the root map so flattened OTel-style label keys resolve naturally.
|
||||
//
|
||||
// Examples (structRoots = {alert, rule}):
|
||||
//
|
||||
// "$name is $status" -> "{{ .name }} is {{ .status }}"
|
||||
// "$labels.severity" -> `{{ index .labels "severity" }}`
|
||||
// "$labels.http.method" -> `{{ index .labels "http.method" }}`
|
||||
// "$annotations.summary" -> `{{ index .annotations "summary" }}`
|
||||
// "$alert.is_firing" -> `{{ index . "alert" "is_firing" }}`
|
||||
// "$rule.threshold.value" -> `{{ index . "rule" "threshold" "value" }}`
|
||||
// "$service.name" -> `{{ index . "service.name" }}`
|
||||
// "$name is {{ .Status }}" -> "{{ .name }} is {{ .Status }}"
|
||||
func wrapDollarVariables(src string, structRoots map[string]bool) (string, error) {
|
||||
if src == "" {
|
||||
return src, nil
|
||||
}
|
||||
|
||||
tree := parse.New("template")
|
||||
tree.Mode = parse.SkipFuncCheck
|
||||
|
||||
if _, err := tree.Parse(src, "{{", "}}", make(map[string]*parse.Tree), nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
walkAndWrapTextNodes(tree.Root, structRoots)
|
||||
return tree.Root.String(), nil
|
||||
}
|
||||
|
||||
// walkAndWrapTextNodes descends the parse tree and rewrites $-references
|
||||
// found in TextNodes. If/Range bodies are recursed into. ActionNodes and
|
||||
// other `{{...}}` constructs are left alone because they're already template
|
||||
// syntax. WithNode is not yet supported — add when the editor grows a "with"
|
||||
// block.
|
||||
func walkAndWrapTextNodes(node parse.Node, structRoots map[string]bool) {
|
||||
if reflect.ValueOf(node).IsNil() {
|
||||
return
|
||||
}
|
||||
|
||||
switch n := node.(type) {
|
||||
case *parse.ListNode:
|
||||
if n.Nodes != nil {
|
||||
for _, child := range n.Nodes {
|
||||
walkAndWrapTextNodes(child, structRoots)
|
||||
}
|
||||
}
|
||||
|
||||
case *parse.TextNode:
|
||||
n.Text = bareVariableRegex.ReplaceAllFunc(n.Text, func(match []byte) []byte {
|
||||
return rewriteDollarRef(match, structRoots)
|
||||
})
|
||||
|
||||
case *parse.IfNode:
|
||||
walkAndWrapTextNodes(n.List, structRoots)
|
||||
walkAndWrapTextNodes(n.ElseList, structRoots)
|
||||
|
||||
case *parse.RangeNode:
|
||||
walkAndWrapTextNodes(n.List, structRoots)
|
||||
walkAndWrapTextNodes(n.ElseList, structRoots)
|
||||
}
|
||||
}
|
||||
|
||||
// rewriteDollarRef converts one $-reference (with the leading $) into the
|
||||
// corresponding Go template expression.
|
||||
//
|
||||
// - labels./annotations. prefixes: treat the remainder as a single map key
|
||||
// (OTel-style keys like "http.request.method" are flat keys, not paths).
|
||||
// - Dotted path with a struct-root first segment: walk via chained index
|
||||
// arguments. `index x a b c` is equivalent to x[a][b][c].
|
||||
// - Other dotted path: treat as a single flat key on the root map, so a
|
||||
// flattened OTel-style label key like "service.name" resolves.
|
||||
// - Simple names: plain dot access on the root map.
|
||||
func rewriteDollarRef(match []byte, structRoots map[string]bool) []byte {
|
||||
name := string(match[1:])
|
||||
|
||||
if !strings.Contains(name, ".") {
|
||||
return fmt.Appendf(nil, `{{ .%s }}`, name)
|
||||
}
|
||||
|
||||
if key, ok := strings.CutPrefix(name, "labels."); ok {
|
||||
return fmt.Appendf(nil, `{{ index .labels %q }}`, key)
|
||||
}
|
||||
if key, ok := strings.CutPrefix(name, "annotations."); ok {
|
||||
return fmt.Appendf(nil, `{{ index .annotations %q }}`, key)
|
||||
}
|
||||
|
||||
// If the first segment is a known struct root, walk segments.
|
||||
if dot := strings.IndexByte(name, '.'); dot >= 0 && structRoots[name[:dot]] {
|
||||
parts := strings.Split(name, ".")
|
||||
args := make([]string, len(parts))
|
||||
for i, p := range parts {
|
||||
args[i] = fmt.Sprintf("%q", p)
|
||||
}
|
||||
return fmt.Appendf(nil, `{{ index . %s }}`, strings.Join(args, " "))
|
||||
}
|
||||
|
||||
// Otherwise treat the full dotted path as a single flat root key.
|
||||
return fmt.Appendf(nil, `{{ index . %q }}`, name)
|
||||
}
|
||||
|
||||
// extractUsedVariables returns the set of every base $-name referenced in the
|
||||
// template — text nodes, action blocks, branch conditions, loop declarations.
|
||||
// Names are first-segment only: $labels.severity contributes "labels".
|
||||
//
|
||||
// The template is validated during extraction (a synthesised preamble
|
||||
// pre-declares each name so the parser doesn't trip on "undefined variable"
|
||||
// for names that genuinely exist in the data), so a parse error here
|
||||
// indicates a genuine syntax problem rather than a missing binding.
|
||||
func extractUsedVariables(src string) (map[string]bool, error) {
|
||||
if src == "" {
|
||||
return map[string]bool{}, nil
|
||||
}
|
||||
|
||||
used := make(map[string]bool)
|
||||
for _, m := range bareVariableFirstSegRegex.FindAll([]byte(src), -1) {
|
||||
used[string(m[1:])] = true
|
||||
}
|
||||
|
||||
var preamble strings.Builder
|
||||
for name := range used {
|
||||
fmt.Fprintf(&preamble, `{{$%s := ""}}`, name)
|
||||
}
|
||||
|
||||
tree := parse.New("template")
|
||||
tree.Mode = parse.SkipFuncCheck
|
||||
if _, err := tree.Parse(preamble.String()+src, "{{", "}}", make(map[string]*parse.Tree), nil); err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInternal, "failed to extract used variables")
|
||||
}
|
||||
|
||||
return used, nil
|
||||
}
|
||||
213
pkg/alertmanager/alertmanagertemplate/rewrite_test.go
Normal file
213
pkg/alertmanager/alertmanagertemplate/rewrite_test.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWrapBareVars(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "mixed variables with actions",
|
||||
input: "$name is {{.Status}}",
|
||||
expected: "{{ .name }} is {{.Status}}",
|
||||
},
|
||||
{
|
||||
name: "nested variables in range",
|
||||
input: `{{range .items}}
|
||||
$title
|
||||
{{end}}`,
|
||||
expected: `{{range .items}}
|
||||
{{ .title }}
|
||||
{{end}}`,
|
||||
},
|
||||
{
|
||||
name: "nested variables in if else",
|
||||
input: "{{if .ok}}$a{{else}}$b{{end}}",
|
||||
expected: "{{if .ok}}{{ .a }}{{else}}{{ .b }}{{end}}",
|
||||
},
|
||||
// Labels prefix: index into .labels map
|
||||
{
|
||||
name: "labels variables prefix simple",
|
||||
input: "$labels.service",
|
||||
expected: `{{ index .labels "service" }}`,
|
||||
},
|
||||
{
|
||||
name: "labels variables prefix nested with multiple dots",
|
||||
input: "$labels.http.status",
|
||||
expected: `{{ index .labels "http.status" }}`,
|
||||
},
|
||||
{
|
||||
name: "multiple labels variables simple and nested",
|
||||
input: "$labels.service and $labels.instance.id",
|
||||
expected: `{{ index .labels "service" }} and {{ index .labels "instance.id" }}`,
|
||||
},
|
||||
// Annotations prefix: index into .annotations map
|
||||
{
|
||||
name: "annotations variables prefix simple",
|
||||
input: "$annotations.summary",
|
||||
expected: `{{ index .annotations "summary" }}`,
|
||||
},
|
||||
{
|
||||
name: "annotations variables prefix nested with multiple dots",
|
||||
input: "$annotations.alert.url",
|
||||
expected: `{{ index .annotations "alert.url" }}`,
|
||||
},
|
||||
// Struct-root paths: walk segment-by-segment via chained index.
|
||||
{
|
||||
name: "struct-root dotted path walks via chained index",
|
||||
input: "$alert.is_firing",
|
||||
expected: `{{ index . "alert" "is_firing" }}`,
|
||||
},
|
||||
{
|
||||
name: "deeply nested struct-root path",
|
||||
input: "$rule.threshold.value",
|
||||
expected: `{{ index . "rule" "threshold" "value" }}`,
|
||||
},
|
||||
// Non-struct-root dotted paths: treated as a single flat key on the
|
||||
// root map, so flattened OTel-style label keys resolve naturally.
|
||||
{
|
||||
name: "non-struct-root dotted path hits flat root key",
|
||||
input: "$service.name",
|
||||
expected: `{{ index . "service.name" }}`,
|
||||
},
|
||||
// Hybrid: all types combined
|
||||
{
|
||||
name: "hybrid - all variables types",
|
||||
input: "Alert: $alert_name Labels: $labels.severity Annotations: $annotations.desc Value: $alert.value Count: $error_count",
|
||||
expected: `Alert: {{ .alert_name }} Labels: {{ index .labels "severity" }} Annotations: {{ index .annotations "desc" }} Value: {{ index . "alert" "value" }} Count: {{ .error_count }}`,
|
||||
},
|
||||
{
|
||||
name: "already wrapped should not be changed",
|
||||
input: "{{$status := .status}}{{.name}} is {{$status | toUpper}}",
|
||||
expected: "{{$status := .status}}{{.name}} is {{$status | toUpper}}",
|
||||
},
|
||||
{
|
||||
name: "no variables should not be changed",
|
||||
input: "Hello world",
|
||||
expected: "Hello world",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "deeply nested",
|
||||
input: "{{range .items}}{{if .ok}}$deep{{end}}{{end}}",
|
||||
expected: "{{range .items}}{{if .ok}}{{ .deep }}{{end}}{{end}}",
|
||||
},
|
||||
{
|
||||
name: "complex example",
|
||||
input: `Hello $name, your score is $score.
|
||||
{{if .isAdmin}}
|
||||
Welcome back $name, you have {{.unreadCount}} messages.
|
||||
{{end}}`,
|
||||
expected: `Hello {{ .name }}, your score is {{ .score }}.
|
||||
{{if .isAdmin}}
|
||||
Welcome back {{ .name }}, you have {{.unreadCount}} messages.
|
||||
{{end}}`,
|
||||
},
|
||||
{
|
||||
name: "with custom function",
|
||||
input: "$name triggered at {{urlescape .url}}",
|
||||
expected: "{{ .name }} triggered at {{urlescape .url}}",
|
||||
},
|
||||
{
|
||||
name: "invalid template",
|
||||
input: "{{invalid",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
// structRoots used across the test cases: "alert" and "rule" are walked,
|
||||
// anything else dotted is treated as a flat root-map key.
|
||||
structRoots := map[string]bool{"alert": true, "rule": true}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := wrapDollarVariables(tc.input, structRoots)
|
||||
|
||||
if tc.expectError {
|
||||
require.Error(t, err, "should error on invalid template syntax")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractUsedVariables(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected map[string]bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "simple usage in text",
|
||||
input: "$name is $status",
|
||||
expected: map[string]bool{"name": true, "status": true},
|
||||
},
|
||||
{
|
||||
name: "declared in action block",
|
||||
input: "{{ $name := .name }}",
|
||||
expected: map[string]bool{"name": true},
|
||||
},
|
||||
{
|
||||
name: "range loop vars",
|
||||
input: "{{ range $i, $v := .items }}{{ end }}",
|
||||
expected: map[string]bool{"i": true, "v": true},
|
||||
},
|
||||
{
|
||||
name: "mixed text and action",
|
||||
input: "$x and {{ $y }}",
|
||||
expected: map[string]bool{"x": true, "y": true},
|
||||
},
|
||||
{
|
||||
name: "dotted path in text extracts base only",
|
||||
input: "$labels.severity",
|
||||
expected: map[string]bool{"labels": true},
|
||||
},
|
||||
{
|
||||
name: "nested if else",
|
||||
input: "{{ if .ok }}{{ $a }}{{ else }}{{ $b }}{{ end }}",
|
||||
expected: map[string]bool{"a": true, "b": true},
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: map[string]bool{},
|
||||
},
|
||||
{
|
||||
name: "no variables",
|
||||
input: "Hello world",
|
||||
expected: map[string]bool{},
|
||||
},
|
||||
{
|
||||
name: "invalid template returns error",
|
||||
input: "{{invalid",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := extractUsedVariables(tc.input)
|
||||
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
@@ -59,6 +60,7 @@ type provider struct {
|
||||
cloudIntegrationHandler cloudintegration.Handler
|
||||
ruleStateHistoryHandler rulestatehistory.Handler
|
||||
alertmanagerHandler alertmanager.Handler
|
||||
rulerHandler ruler.Handler
|
||||
}
|
||||
|
||||
func NewFactory(
|
||||
@@ -86,6 +88,7 @@ func NewFactory(
|
||||
cloudIntegrationHandler cloudintegration.Handler,
|
||||
ruleStateHistoryHandler rulestatehistory.Handler,
|
||||
alertmanagerHandler alertmanager.Handler,
|
||||
rulerHandler ruler.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(
|
||||
@@ -116,6 +119,7 @@ func NewFactory(
|
||||
cloudIntegrationHandler,
|
||||
ruleStateHistoryHandler,
|
||||
alertmanagerHandler,
|
||||
rulerHandler,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -148,6 +152,7 @@ func newProvider(
|
||||
cloudIntegrationHandler cloudintegration.Handler,
|
||||
ruleStateHistoryHandler rulestatehistory.Handler,
|
||||
alertmanagerHandler alertmanager.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
) (apiserver.APIServer, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
|
||||
router := mux.NewRouter().UseEncodedPath()
|
||||
@@ -178,6 +183,7 @@ func newProvider(
|
||||
cloudIntegrationHandler: cloudIntegrationHandler,
|
||||
ruleStateHistoryHandler: ruleStateHistoryHandler,
|
||||
alertmanagerHandler: alertmanagerHandler,
|
||||
rulerHandler: rulerHandler,
|
||||
}
|
||||
|
||||
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
|
||||
@@ -282,6 +288,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addRulerRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
183
pkg/apiserver/signozapiserver/ruler.go
Normal file
183
pkg/apiserver/signozapiserver/ruler.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addRulerRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/rules", handler.New(provider.authZ.ViewAccess(provider.rulerHandler.ListRules), handler.OpenAPIDef{
|
||||
ID: "ListRules",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "List alert rules",
|
||||
Description: "This endpoint lists all alert rules with their current evaluation state",
|
||||
Response: new(ruletypes.GettableRules),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authZ.ViewAccess(provider.rulerHandler.GetRuleByID), handler.OpenAPIDef{
|
||||
ID: "GetRuleByID",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Get alert rule by ID",
|
||||
Description: "This endpoint returns an alert rule by ID",
|
||||
Response: new(ruletypes.GettableRule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rules", handler.New(provider.authZ.EditAccess(provider.rulerHandler.CreateRule), handler.OpenAPIDef{
|
||||
ID: "CreateRule",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Create alert rule",
|
||||
Description: "This endpoint creates a new alert rule",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(ruletypes.GettableRule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authZ.EditAccess(provider.rulerHandler.UpdateRuleByID), handler.OpenAPIDef{
|
||||
ID: "UpdateRuleByID",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Update alert rule",
|
||||
Description: "This endpoint updates an alert rule by ID",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authZ.EditAccess(provider.rulerHandler.DeleteRuleByID), handler.OpenAPIDef{
|
||||
ID: "DeleteRuleByID",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Delete alert rule",
|
||||
Description: "This endpoint deletes an alert rule by ID",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authZ.EditAccess(provider.rulerHandler.PatchRuleByID), handler.OpenAPIDef{
|
||||
ID: "PatchRuleByID",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Patch alert rule",
|
||||
Description: "This endpoint applies a partial update to an alert rule by ID",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(ruletypes.GettableRule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rules/test", handler.New(provider.authZ.EditAccess(provider.rulerHandler.TestRule), handler.OpenAPIDef{
|
||||
ID: "TestRule",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Test alert rule",
|
||||
Description: "This endpoint fires a test notification for the given rule definition",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(ruletypes.GettableTestRule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/downtime_schedules", handler.New(provider.authZ.ViewAccess(provider.rulerHandler.ListDowntimeSchedules), handler.OpenAPIDef{
|
||||
ID: "ListDowntimeSchedules",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "List downtime schedules",
|
||||
Description: "This endpoint lists all planned maintenance / downtime schedules",
|
||||
RequestQuery: new(ruletypes.ListPlannedMaintenanceParams),
|
||||
Response: make([]*ruletypes.GettablePlannedMaintenance, 0),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authZ.ViewAccess(provider.rulerHandler.GetDowntimeScheduleByID), handler.OpenAPIDef{
|
||||
ID: "GetDowntimeScheduleByID",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "Get downtime schedule by ID",
|
||||
Description: "This endpoint returns a downtime schedule by ID",
|
||||
Response: new(ruletypes.GettablePlannedMaintenance),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/downtime_schedules", handler.New(provider.authZ.EditAccess(provider.rulerHandler.CreateDowntimeSchedule), handler.OpenAPIDef{
|
||||
ID: "CreateDowntimeSchedule",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "Create downtime schedule",
|
||||
Description: "This endpoint creates a new planned maintenance / downtime schedule",
|
||||
Request: new(ruletypes.GettablePlannedMaintenance),
|
||||
RequestContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authZ.EditAccess(provider.rulerHandler.UpdateDowntimeScheduleByID), handler.OpenAPIDef{
|
||||
ID: "UpdateDowntimeScheduleByID",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "Update downtime schedule",
|
||||
Description: "This endpoint updates a downtime schedule by ID",
|
||||
Request: new(ruletypes.GettablePlannedMaintenance),
|
||||
RequestContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authZ.EditAccess(provider.rulerHandler.DeleteDowntimeScheduleByID), handler.OpenAPIDef{
|
||||
ID: "DeleteDowntimeScheduleByID",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "Delete downtime schedule",
|
||||
Description: "This endpoint deletes a downtime schedule by ID",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1070,11 +1070,23 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
|
||||
|
||||
serviceNameToTotalDurationMap = tracedetail.CalculateServiceTime(serviceNameIntervalMap)
|
||||
|
||||
// TODO: set the span data (model.GetWaterfallSpansForTraceWithMetadataCache) in cache here
|
||||
// removed existing cache usage since it was not getting used due to this bug https://github.com/SigNoz/engineering-pod/issues/4648
|
||||
// and was causing out of memory issues https://github.com/SigNoz/engineering-pod/issues/4638
|
||||
traceCache := model.GetWaterfallSpansForTraceWithMetadataCache{
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
DurationNano: durationNano,
|
||||
TotalSpans: totalSpans,
|
||||
TotalErrorSpans: totalErrorSpans,
|
||||
SpanIdToSpanNodeMap: spanIdToSpanNodeMap,
|
||||
ServiceNameToTotalDurationMap: serviceNameToTotalDurationMap,
|
||||
TraceRoots: traceRoots,
|
||||
HasMissingSpans: hasMissingSpans,
|
||||
}
|
||||
|
||||
r.logger.Info("getWaterfallSpansForTraceWithMetadata: processing pre cache", "duration", time.Since(processingBeforeCache), "traceID", traceID)
|
||||
cacheErr := r.cacheForTraceDetail.Set(ctx, orgID, strings.Join([]string{"getWaterfallSpansForTraceWithMetadata", traceID}, "-"), &traceCache, time.Minute*5)
|
||||
if cacheErr != nil {
|
||||
r.logger.Debug("failed to store cache for getWaterfallSpansForTraceWithMetadata", "traceID", traceID, errorsV2.Attr(err))
|
||||
}
|
||||
}
|
||||
|
||||
processingPostCache := time.Now()
|
||||
@@ -1247,13 +1259,19 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, orgID
|
||||
}
|
||||
|
||||
selectedSpans = tracedetail.GetAllSpansForFlamegraph(traceRoots, spanIdToSpanNodeMap)
|
||||
|
||||
// TODO: set the trace data (model.GetFlamegraphSpansForTraceCache) in cache here
|
||||
// removed existing cache usage since it was not getting used due to this bug https://github.com/SigNoz/engineering-pod/issues/4648
|
||||
// and was causing out of memory issues https://github.com/SigNoz/engineering-pod/issues/4638
|
||||
traceCache := model.GetFlamegraphSpansForTraceCache{
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
DurationNano: durationNano,
|
||||
SelectedSpans: selectedSpans,
|
||||
TraceRoots: traceRoots,
|
||||
}
|
||||
|
||||
r.logger.Info("getFlamegraphSpansForTrace: processing pre cache", "duration", time.Since(processingBeforeCache), "traceID", traceID)
|
||||
|
||||
cacheErr := r.cacheForTraceDetail.Set(ctx, orgID, strings.Join([]string{"getFlamegraphSpansForTrace", traceID}, "-"), &traceCache, time.Minute*5)
|
||||
if cacheErr != nil {
|
||||
r.logger.Debug("failed to store cache for getFlamegraphSpansForTrace", "traceID", traceID, errorsV2.Attr(err))
|
||||
}
|
||||
}
|
||||
|
||||
processingPostCache := time.Now()
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/modules/thirdpartyapi"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
|
||||
"io"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net/http"
|
||||
@@ -78,7 +78,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
)
|
||||
|
||||
@@ -98,7 +98,7 @@ func NewRouter() *mux.Router {
|
||||
type APIHandler struct {
|
||||
logger *slog.Logger
|
||||
reader interfaces.Reader
|
||||
ruleManager *rules.Manager
|
||||
ruleManager ruler.Ruler
|
||||
querier interfaces.Querier
|
||||
querierV2 interfaces.Querier
|
||||
queryBuilder *queryBuilder.QueryBuilder
|
||||
@@ -150,9 +150,6 @@ type APIHandlerOpts struct {
|
||||
// business data reader e.g. clickhouse
|
||||
Reader interfaces.Reader
|
||||
|
||||
// rule manager handles rule crud operations
|
||||
RuleManager *rules.Manager
|
||||
|
||||
// Integrations
|
||||
IntegrationsController *integrations.Controller
|
||||
|
||||
@@ -208,7 +205,7 @@ func NewAPIHandler(opts APIHandlerOpts, config signoz.Config) (*APIHandler, erro
|
||||
logger: slog.Default(),
|
||||
reader: opts.Reader,
|
||||
temporalityMap: make(map[string]map[v3.Temporality]bool),
|
||||
ruleManager: opts.RuleManager,
|
||||
ruleManager: opts.Signoz.Ruler,
|
||||
IntegrationsController: opts.IntegrationsController,
|
||||
CloudIntegrationsController: opts.CloudIntegrationsController,
|
||||
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
||||
@@ -508,12 +505,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
router.HandleFunc("/api/v1/rules/{id}/history/top_contributors", am.ViewAccess(aH.getRuleStateHistoryTopContributors)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/rules/{id}/history/overall_status", am.ViewAccess(aH.getOverallStateTransitions)).Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v1/downtime_schedules", am.ViewAccess(aH.listDowntimeSchedules)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.ViewAccess(aH.getDowntimeSchedule)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/downtime_schedules", am.EditAccess(aH.createDowntimeSchedule)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.EditAccess(aH.editDowntimeSchedule)).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.EditAccess(aH.deleteDowntimeSchedule)).Methods(http.MethodDelete)
|
||||
|
||||
router.HandleFunc("/api/v1/dashboards", am.ViewAccess(aH.List)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/dashboards", am.EditAccess(aH.Signoz.Handlers.Dashboard.Create)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/dashboards/{id}", am.ViewAccess(aH.Get)).Methods(http.MethodGet)
|
||||
@@ -603,26 +594,6 @@ func Intersection(a, b []int) (c []int) {
|
||||
return
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getRule(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := valuer.NewUUID(idStr)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ruleResponse, err := aH.ruleManager.GetRule(r.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
|
||||
return
|
||||
}
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
aH.Respond(w, ruleResponse)
|
||||
}
|
||||
|
||||
// populateTemporality adds the temporality to the query if it is not present
|
||||
func (aH *APIHandler) PopulateTemporality(ctx context.Context, orgID valuer.UUID, qp *v3.QueryRangeParamsV3) error {
|
||||
|
||||
@@ -678,127 +649,164 @@ func (aH *APIHandler) PopulateTemporality(ctx context.Context, orgID valuer.UUID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (aH *APIHandler) listDowntimeSchedules(w http.ResponseWriter, r *http.Request) {
|
||||
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
|
||||
if errv2 != nil {
|
||||
render.Error(w, errv2)
|
||||
return
|
||||
}
|
||||
func (aH *APIHandler) listRules(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
schedules, err := aH.ruleManager.MaintenanceStore().GetAllPlannedMaintenance(r.Context(), claims.OrgID)
|
||||
rules, err := aH.ruleManager.ListRuleStates(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// The schedules are stored as JSON in the database, so we need to filter them here
|
||||
// Since the number of schedules is expected to be small, this should be fine
|
||||
// todo(amol): need to add sorter
|
||||
|
||||
if r.URL.Query().Get("active") != "" {
|
||||
activeSchedules := make([]*ruletypes.GettablePlannedMaintenance, 0)
|
||||
active, _ := strconv.ParseBool(r.URL.Query().Get("active"))
|
||||
for _, schedule := range schedules {
|
||||
now := time.Now().In(time.FixedZone(schedule.Schedule.Timezone, 0))
|
||||
if schedule.IsActive(now) == active {
|
||||
activeSchedules = append(activeSchedules, schedule)
|
||||
}
|
||||
}
|
||||
schedules = activeSchedules
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("recurring") != "" {
|
||||
recurringSchedules := make([]*ruletypes.GettablePlannedMaintenance, 0)
|
||||
recurring, _ := strconv.ParseBool(r.URL.Query().Get("recurring"))
|
||||
for _, schedule := range schedules {
|
||||
if schedule.IsRecurring() == recurring {
|
||||
recurringSchedules = append(recurringSchedules, schedule)
|
||||
}
|
||||
}
|
||||
schedules = recurringSchedules
|
||||
}
|
||||
|
||||
aH.Respond(w, schedules)
|
||||
aH.Respond(w, rules)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getDowntimeSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
func (aH *APIHandler) getRule(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := valuer.NewUUID(idStr)
|
||||
if err != nil {
|
||||
render.Error(w, errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
schedule, err := aH.ruleManager.MaintenanceStore().GetPlannedMaintenanceByID(r.Context(), id)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
aH.Respond(w, schedule)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) createDowntimeSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
var schedule ruletypes.GettablePlannedMaintenance
|
||||
err := json.NewDecoder(r.Body).Decode(&schedule)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
if err := schedule.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = aH.ruleManager.MaintenanceStore().CreatePlannedMaintenance(r.Context(), schedule)
|
||||
ruleResponse, err := aH.ruleManager.GetRule(r.Context(), id)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
|
||||
return
|
||||
}
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
aH.Respond(w, nil)
|
||||
aH.Respond(w, ruleResponse)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) editDowntimeSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
func (aH *APIHandler) createRule(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
aH.logger.ErrorContext(r.Context(), "error reading request body for create rule", errors.Attr(err))
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
rule, err := aH.ruleManager.CreateRule(r.Context(), string(body))
|
||||
if err != nil {
|
||||
RespondError(w, toApiError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, rule)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) editRule(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := valuer.NewUUID(idStr)
|
||||
if err != nil {
|
||||
render.Error(w, errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error()))
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var schedule ruletypes.GettablePlannedMaintenance
|
||||
err = json.NewDecoder(r.Body).Decode(&schedule)
|
||||
defer r.Body.Close()
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
if err := schedule.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
aH.logger.ErrorContext(r.Context(), "error reading request body for edit rule", errors.Attr(err))
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = aH.ruleManager.MaintenanceStore().EditPlannedMaintenance(r.Context(), schedule, id)
|
||||
err = aH.ruleManager.EditRule(r.Context(), string(body), id)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
|
||||
return
|
||||
}
|
||||
RespondError(w, toApiError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, nil)
|
||||
aH.Respond(w, "rule successfully edited")
|
||||
}
|
||||
|
||||
func (aH *APIHandler) deleteDowntimeSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
func (aH *APIHandler) deleteRule(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
err := aH.ruleManager.DeleteRule(r.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
|
||||
return
|
||||
}
|
||||
RespondError(w, toApiError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, "rule successfully deleted")
|
||||
}
|
||||
|
||||
func (aH *APIHandler) patchRule(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := valuer.NewUUID(idStr)
|
||||
if err != nil {
|
||||
render.Error(w, errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error()))
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = aH.ruleManager.MaintenanceStore().DeletePlannedMaintenance(r.Context(), id)
|
||||
defer r.Body.Close()
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
aH.logger.ErrorContext(r.Context(), "error reading request body for patch rule", errors.Attr(err))
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
gettableRule, err := aH.ruleManager.PatchRule(r.Context(), string(body), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
|
||||
return
|
||||
}
|
||||
RespondError(w, toApiError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, gettableRule)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) testRule(w http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
aH.logger.ErrorContext(r.Context(), "error reading request body for test rule", errors.Attr(err))
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, nil)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
alertCount, err := aH.ruleManager.TestNotification(ctx, orgID, string(body))
|
||||
if err != nil {
|
||||
RespondError(w, toApiError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"alertCount": alertCount,
|
||||
"message": "notification sent",
|
||||
}
|
||||
aH.Respond(w, response)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getRuleStats(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1012,19 +1020,6 @@ func (aH *APIHandler) getRuleStateHistoryTopContributors(w http.ResponseWriter,
|
||||
aH.Respond(w, res)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) listRules(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
rules, err := aH.ruleManager.ListRuleStates(r.Context())
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// todo(amol): need to add sorter
|
||||
|
||||
aH.Respond(w, rules)
|
||||
}
|
||||
|
||||
func prepareQuery(r *http.Request) (string, error) {
|
||||
var postData *model.DashboardVars
|
||||
|
||||
@@ -1226,142 +1221,6 @@ func (aH *APIHandler) queryDashboardVarsV2(w http.ResponseWriter, r *http.Reques
|
||||
aH.Respond(w, dashboardVars)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) testRule(w http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
aH.logger.ErrorContext(r.Context(), "error reading request body for test rule", errors.Attr(err))
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
alertCount, err := aH.ruleManager.TestNotification(ctx, orgID, string(body))
|
||||
if err != nil {
|
||||
RespondError(w, toApiError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"alertCount": alertCount,
|
||||
"message": "notification sent",
|
||||
}
|
||||
aH.Respond(w, response)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) deleteRule(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
err := aH.ruleManager.DeleteRule(r.Context(), id)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
|
||||
return
|
||||
}
|
||||
RespondError(w, toApiError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, "rule successfully deleted")
|
||||
|
||||
}
|
||||
|
||||
// patchRule updates only requested changes in the rule
|
||||
func (aH *APIHandler) patchRule(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := valuer.NewUUID(idStr)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
aH.logger.ErrorContext(r.Context(), "error reading request body for patch rule", errors.Attr(err))
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
gettableRule, err := aH.ruleManager.PatchRule(r.Context(), string(body), id)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
|
||||
return
|
||||
}
|
||||
RespondError(w, toApiError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, gettableRule)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) editRule(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := valuer.NewUUID(idStr)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
aH.logger.ErrorContext(r.Context(), "error reading request body for edit rule", errors.Attr(err))
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = aH.ruleManager.EditRule(r.Context(), string(body), id)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
|
||||
return
|
||||
}
|
||||
RespondError(w, toApiError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, "rule successfully edited")
|
||||
|
||||
}
|
||||
|
||||
func (aH *APIHandler) createRule(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
defer r.Body.Close()
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
aH.logger.ErrorContext(r.Context(), "error reading request body for create rule", errors.Attr(err))
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
rule, err := aH.ruleManager.CreateRule(r.Context(), string(body))
|
||||
if err != nil {
|
||||
RespondError(w, toApiError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, rule)
|
||||
|
||||
}
|
||||
|
||||
func (aH *APIHandler) queryRangeMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
query, apiErrorObj := parseQueryRangeRequest(r)
|
||||
|
||||
@@ -9,24 +9,16 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/cache/memorycache"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
|
||||
"github.com/rs/cors"
|
||||
"github.com/soheilhy/cmux"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||
"github.com/SigNoz/signoz/pkg/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"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
@@ -34,10 +26,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/opamp"
|
||||
opAmpModel "github.com/SigNoz/signoz/pkg/query-service/app/opamp/model"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/web"
|
||||
|
||||
"log/slog"
|
||||
@@ -47,15 +36,13 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
)
|
||||
|
||||
// Server runs HTTP, Mux and a grpc server
|
||||
type Server struct {
|
||||
config signoz.Config
|
||||
signoz *signoz.SigNoz
|
||||
ruleManager *rules.Manager
|
||||
config signoz.Config
|
||||
signoz *signoz.SigNoz
|
||||
|
||||
// public http router
|
||||
httpConn net.Listener
|
||||
@@ -102,24 +89,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
nil,
|
||||
)
|
||||
|
||||
rm, err := makeRulesManager(
|
||||
reader,
|
||||
signoz.Cache,
|
||||
signoz.Alertmanager,
|
||||
signoz.SQLStore,
|
||||
signoz.TelemetryStore,
|
||||
signoz.TelemetryMetadataStore,
|
||||
signoz.Prometheus,
|
||||
signoz.Modules.OrgGetter,
|
||||
signoz.Modules.RuleStateHistory,
|
||||
signoz.Querier,
|
||||
signoz.Instrumentation.ToProviderSettings(),
|
||||
signoz.QueryParser,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
|
||||
signoz.SQLStore,
|
||||
integrationsController.GetPipelinesForInstalledIntegrations,
|
||||
@@ -131,7 +100,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
|
||||
apiHandler, err := NewAPIHandler(APIHandlerOpts{
|
||||
Reader: reader,
|
||||
RuleManager: rm,
|
||||
IntegrationsController: integrationsController,
|
||||
CloudIntegrationsController: cloudIntegrationsController,
|
||||
LogsParsingPipelineController: logParsingPipelineController,
|
||||
@@ -146,8 +114,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
|
||||
s := &Server{
|
||||
config: config,
|
||||
signoz: signoz,
|
||||
ruleManager: rm,
|
||||
signoz: signoz,
|
||||
httpHostPort: constants.HTTPHostPort,
|
||||
unavailableChannel: make(chan healthcheck.Status),
|
||||
}
|
||||
@@ -270,8 +237,6 @@ func (s *Server) initListeners() error {
|
||||
|
||||
// Start listening on http and private http port concurrently
|
||||
func (s *Server) Start(ctx context.Context) error {
|
||||
s.ruleManager.Start(ctx)
|
||||
|
||||
err := s.initListeners()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -315,55 +280,6 @@ func (s *Server) Stop(ctx context.Context) error {
|
||||
|
||||
s.opampServer.Stop()
|
||||
|
||||
if s.ruleManager != nil {
|
||||
s.ruleManager.Stop(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeRulesManager(
|
||||
ch interfaces.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,
|
||||
) (*rules.Manager, error) {
|
||||
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
||||
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
|
||||
// create manager opts
|
||||
managerOpts := &rules.ManagerOptions{
|
||||
TelemetryStore: telemetryStore,
|
||||
MetadataStore: metadataStore,
|
||||
Prometheus: prometheus,
|
||||
Context: context.Background(),
|
||||
Querier: querier,
|
||||
Logger: providerSettings.Logger,
|
||||
Cache: cache,
|
||||
EvalDelay: constants.GetEvalDelay(),
|
||||
OrgGetter: orgGetter,
|
||||
Alertmanager: alertmanager,
|
||||
RuleStore: ruleStore,
|
||||
MaintenanceStore: maintenanceStore,
|
||||
SQLStore: sqlstore,
|
||||
QueryParser: queryParser,
|
||||
RuleStateHistoryModule: ruleStateHistoryModule,
|
||||
}
|
||||
|
||||
// create Manager
|
||||
manager, err := rules.NewManager(managerOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rule manager error: %v", err)
|
||||
}
|
||||
|
||||
slog.Info("rules manager is ready")
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package ruler
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
EvalDelay time.Duration `mapstructure:"eval_delay"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
@@ -12,7 +15,9 @@ func NewConfigFactory() factory.ConfigFactory {
|
||||
}
|
||||
|
||||
func newConfig() factory.Config {
|
||||
return Config{}
|
||||
return Config{
|
||||
EvalDelay: 2 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
|
||||
19
pkg/ruler/handler.go
Normal file
19
pkg/ruler/handler.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package ruler
|
||||
|
||||
import "net/http"
|
||||
|
||||
type Handler interface {
|
||||
ListRules(http.ResponseWriter, *http.Request)
|
||||
GetRuleByID(http.ResponseWriter, *http.Request)
|
||||
CreateRule(http.ResponseWriter, *http.Request)
|
||||
UpdateRuleByID(http.ResponseWriter, *http.Request)
|
||||
DeleteRuleByID(http.ResponseWriter, *http.Request)
|
||||
PatchRuleByID(http.ResponseWriter, *http.Request)
|
||||
TestRule(http.ResponseWriter, *http.Request)
|
||||
|
||||
ListDowntimeSchedules(http.ResponseWriter, *http.Request)
|
||||
GetDowntimeScheduleByID(http.ResponseWriter, *http.Request)
|
||||
CreateDowntimeSchedule(http.ResponseWriter, *http.Request)
|
||||
UpdateDowntimeScheduleByID(http.ResponseWriter, *http.Request)
|
||||
DeleteDowntimeScheduleByID(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
@@ -1,7 +1,48 @@
|
||||
package ruler
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/statsreporter"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/statsreporter"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Ruler interface {
|
||||
factory.ServiceWithHealthy
|
||||
statsreporter.StatsCollector
|
||||
|
||||
// ListRuleStates returns all rules with their current evaluation state.
|
||||
ListRuleStates(ctx context.Context) (*ruletypes.GettableRules, error)
|
||||
|
||||
// GetRule returns a single rule by ID.
|
||||
GetRule(ctx context.Context, id valuer.UUID) (*ruletypes.GettableRule, error)
|
||||
|
||||
// CreateRule persists a new rule from a JSON string and starts its evaluator.
|
||||
// TODO: accept PostableRule instead of raw string; the manager currently unmarshals
|
||||
// internally because it stores the raw JSON as Data. Requires changing the storage
|
||||
// model to store structured data.
|
||||
CreateRule(ctx context.Context, ruleStr string) (*ruletypes.GettableRule, error)
|
||||
|
||||
// EditRule replaces the rule identified by id with the given JSON string.
|
||||
// TODO: same as CreateRule — accept PostableRule instead of raw string.
|
||||
EditRule(ctx context.Context, ruleStr string, id valuer.UUID) error
|
||||
|
||||
// DeleteRule removes the rule identified by the string ID.
|
||||
// TODO: accept valuer.UUID instead of string for consistency with other methods.
|
||||
DeleteRule(ctx context.Context, idStr string) error
|
||||
|
||||
// PatchRule applies a partial update to the rule identified by id.
|
||||
// TODO: same as CreateRule — accept PostableRule instead of raw string.
|
||||
PatchRule(ctx context.Context, ruleStr string, id valuer.UUID) (*ruletypes.GettableRule, error)
|
||||
|
||||
// TestNotification fires a test alert for the rule defined in ruleStr.
|
||||
// TODO: same as CreateRule — accept PostableRule instead of raw string.
|
||||
TestNotification(ctx context.Context, orgID valuer.UUID, ruleStr string) (int, error)
|
||||
|
||||
// MaintenanceStore returns the store for planned maintenance / downtime schedules.
|
||||
// TODO: expose downtime CRUD as methods on Ruler directly instead of leaking the
|
||||
// store interface. The handler should not call store methods directly.
|
||||
MaintenanceStore() ruletypes.MaintenanceStore
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (r *maintenance) GetPlannedMaintenanceByID(ctx context.Context, id valuer.U
|
||||
Where("id = ?", id.StringValue()).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "planned maintenance with ID: %s does not exist", id.StringValue())
|
||||
}
|
||||
|
||||
return storableMaintenanceRule.ConvertGettableMaintenanceRuleToGettableMaintenance(), nil
|
||||
|
||||
@@ -111,7 +111,7 @@ func (r *rule) GetStoredRule(ctx context.Context, id valuer.UUID) (*ruletypes.Ru
|
||||
Where("id = ?", id.StringValue()).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "rule with ID: %s does not exist", id.StringValue())
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
314
pkg/ruler/signozruler/handler.go
Normal file
314
pkg/ruler/signozruler/handler.go
Normal file
@@ -0,0 +1,314 @@
|
||||
package signozruler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
ruler ruler.Ruler
|
||||
}
|
||||
|
||||
func NewHandler(ruler ruler.Ruler) ruler.Handler {
|
||||
return &handler{ruler: ruler}
|
||||
}
|
||||
|
||||
func (handler *handler) ListRules(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rules, err := handler.ruler.ListRuleStates(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, rules)
|
||||
}
|
||||
|
||||
func (handler *handler) GetRuleByID(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(req)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
||||
return
|
||||
}
|
||||
|
||||
rule, err := handler.ruler.GetRule(ctx, id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, rule)
|
||||
}
|
||||
|
||||
func (handler *handler) CreateRule(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
defer req.Body.Close() //nolint:errcheck
|
||||
|
||||
rule, err := handler.ruler.CreateRule(ctx, string(body))
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, rule)
|
||||
}
|
||||
|
||||
func (handler *handler) UpdateRuleByID(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(req)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
defer req.Body.Close() //nolint:errcheck
|
||||
|
||||
err = handler.ruler.EditRule(ctx, string(body), id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) DeleteRuleByID(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
idStr := mux.Vars(req)["id"]
|
||||
|
||||
err := handler.ruler.DeleteRule(ctx, idStr)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) PatchRuleByID(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(req)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
defer req.Body.Close() //nolint:errcheck
|
||||
|
||||
rule, err := handler.ruler.PatchRule(ctx, string(body), id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, rule)
|
||||
}
|
||||
|
||||
func (handler *handler) TestRule(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 1*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
defer req.Body.Close() //nolint:errcheck
|
||||
|
||||
alertCount, err := handler.ruler.TestNotification(ctx, orgID, string(body))
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, ruletypes.GettableTestRule{AlertCount: alertCount, Message: "notification sent"})
|
||||
}
|
||||
|
||||
func (handler *handler) ListDowntimeSchedules(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var params ruletypes.ListPlannedMaintenanceParams
|
||||
if err := binding.Query.BindQuery(req.URL.Query(), ¶ms); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
schedules, err := handler.ruler.MaintenanceStore().GetAllPlannedMaintenance(ctx, claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if params.Active != nil {
|
||||
activeSchedules := make([]*ruletypes.GettablePlannedMaintenance, 0)
|
||||
for _, schedule := range schedules {
|
||||
now := time.Now().In(time.FixedZone(schedule.Schedule.Timezone, 0))
|
||||
if schedule.IsActive(now) == *params.Active {
|
||||
activeSchedules = append(activeSchedules, schedule)
|
||||
}
|
||||
}
|
||||
schedules = activeSchedules
|
||||
}
|
||||
|
||||
if params.Recurring != nil {
|
||||
recurringSchedules := make([]*ruletypes.GettablePlannedMaintenance, 0)
|
||||
for _, schedule := range schedules {
|
||||
if schedule.IsRecurring() == *params.Recurring {
|
||||
recurringSchedules = append(recurringSchedules, schedule)
|
||||
}
|
||||
}
|
||||
schedules = recurringSchedules
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, schedules)
|
||||
}
|
||||
|
||||
func (handler *handler) GetDowntimeScheduleByID(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(req)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
||||
return
|
||||
}
|
||||
|
||||
schedule, err := handler.ruler.MaintenanceStore().GetPlannedMaintenanceByID(ctx, id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, schedule)
|
||||
}
|
||||
|
||||
func (handler *handler) CreateDowntimeSchedule(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var schedule ruletypes.GettablePlannedMaintenance
|
||||
if err := binding.JSON.BindBody(req.Body, &schedule); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := schedule.Validate(); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := handler.ruler.MaintenanceStore().CreatePlannedMaintenance(ctx, schedule)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) UpdateDowntimeScheduleByID(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(req)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
||||
return
|
||||
}
|
||||
|
||||
var schedule ruletypes.GettablePlannedMaintenance
|
||||
if err := binding.JSON.BindBody(req.Body, &schedule); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := schedule.Validate(); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.ruler.MaintenanceStore().EditPlannedMaintenance(ctx, schedule, id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) DeleteDowntimeScheduleByID(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(req)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.ruler.MaintenanceStore().DeletePlannedMaintenance(ctx, id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
@@ -3,27 +3,93 @@ package signozruler
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"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/rules"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
manager *rules.Manager
|
||||
ruleStore ruletypes.RuleStore
|
||||
stopC chan struct{}
|
||||
healthyC chan struct{}
|
||||
}
|
||||
|
||||
func NewFactory(sqlstore sqlstore.SQLStore, queryParser queryparser.QueryParser) factory.ProviderFactory[ruler.Ruler, ruler.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, settings factory.ProviderSettings, config ruler.Config) (ruler.Ruler, error) {
|
||||
return New(ctx, settings, config, sqlstore, queryParser)
|
||||
func NewFactory(
|
||||
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,
|
||||
queryParser queryparser.QueryParser,
|
||||
prepareTaskFunc func(rules.PrepareTaskOptions) (rules.Task, error),
|
||||
prepareTestRuleFunc func(rules.PrepareTestRuleOptions) (int, error),
|
||||
) factory.ProviderFactory[ruler.Ruler, ruler.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config ruler.Config) (ruler.Ruler, error) {
|
||||
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
||||
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
|
||||
|
||||
managerOpts := &rules.ManagerOptions{
|
||||
TelemetryStore: telemetryStore,
|
||||
MetadataStore: metadataStore,
|
||||
Prometheus: prometheus,
|
||||
Context: context.Background(),
|
||||
Querier: querier,
|
||||
Logger: providerSettings.Logger,
|
||||
Cache: cache,
|
||||
EvalDelay: valuer.MustParseTextDuration(config.EvalDelay.String()),
|
||||
PrepareTaskFunc: prepareTaskFunc,
|
||||
PrepareTestRuleFunc: prepareTestRuleFunc,
|
||||
Alertmanager: alertmanager,
|
||||
OrgGetter: orgGetter,
|
||||
RuleStore: ruleStore,
|
||||
MaintenanceStore: maintenanceStore,
|
||||
SQLStore: sqlstore,
|
||||
QueryParser: queryParser,
|
||||
RuleStateHistoryModule: ruleStateHistoryModule,
|
||||
}
|
||||
|
||||
manager, err := rules.NewManager(managerOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &provider{manager: manager, ruleStore: ruleStore, stopC: make(chan struct{}), healthyC: make(chan struct{})}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func New(ctx context.Context, settings factory.ProviderSettings, config ruler.Config, sqlstore sqlstore.SQLStore, queryParser queryparser.QueryParser) (ruler.Ruler, error) {
|
||||
return &provider{ruleStore: sqlrulestore.NewRuleStore(sqlstore, queryParser, settings)}, nil
|
||||
func (provider *provider) Start(ctx context.Context) error {
|
||||
provider.manager.Start(ctx)
|
||||
close(provider.healthyC)
|
||||
<-provider.stopC
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) Healthy() <-chan struct{} {
|
||||
return provider.healthyC
|
||||
}
|
||||
|
||||
func (provider *provider) Stop(ctx context.Context) error {
|
||||
close(provider.stopC)
|
||||
provider.manager.Stop(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
|
||||
@@ -34,3 +100,35 @@ func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[s
|
||||
|
||||
return ruletypes.NewStatsFromRules(rules), nil
|
||||
}
|
||||
|
||||
func (provider *provider) ListRuleStates(ctx context.Context) (*ruletypes.GettableRules, error) {
|
||||
return provider.manager.ListRuleStates(ctx)
|
||||
}
|
||||
|
||||
func (provider *provider) GetRule(ctx context.Context, id valuer.UUID) (*ruletypes.GettableRule, error) {
|
||||
return provider.manager.GetRule(ctx, id)
|
||||
}
|
||||
|
||||
func (provider *provider) CreateRule(ctx context.Context, ruleStr string) (*ruletypes.GettableRule, error) {
|
||||
return provider.manager.CreateRule(ctx, ruleStr)
|
||||
}
|
||||
|
||||
func (provider *provider) EditRule(ctx context.Context, ruleStr string, id valuer.UUID) error {
|
||||
return provider.manager.EditRule(ctx, ruleStr, id)
|
||||
}
|
||||
|
||||
func (provider *provider) DeleteRule(ctx context.Context, idStr string) error {
|
||||
return provider.manager.DeleteRule(ctx, idStr)
|
||||
}
|
||||
|
||||
func (provider *provider) PatchRule(ctx context.Context, ruleStr string, id valuer.UUID) (*ruletypes.GettableRule, error) {
|
||||
return provider.manager.PatchRule(ctx, ruleStr, id)
|
||||
}
|
||||
|
||||
func (provider *provider) TestNotification(ctx context.Context, orgID valuer.UUID, ruleStr string) (int, error) {
|
||||
return provider.manager.TestNotification(ctx, orgID, ruleStr)
|
||||
}
|
||||
|
||||
func (provider *provider) MaintenanceStore() ruletypes.MaintenanceStore {
|
||||
return provider.manager.MaintenanceStore()
|
||||
}
|
||||
|
||||
@@ -305,6 +305,15 @@ func mergeAndEnsureBackwardCompatibility(ctx context.Context, logger *slog.Logge
|
||||
}
|
||||
config.Flagger.Config.Boolean[flagger.FeatureKafkaSpanEval.String()] = os.Getenv("KAFKA_SPAN_EVAL") == "true"
|
||||
}
|
||||
|
||||
if os.Getenv("RULES_EVAL_DELAY") != "" {
|
||||
logger.WarnContext(ctx, "[Deprecated] env RULES_EVAL_DELAY is deprecated and scheduled for removal. Please use SIGNOZ_RULER_EVAL__DELAY instead.")
|
||||
if d, err := time.ParseDuration(os.Getenv("RULES_EVAL_DELAY")); err == nil {
|
||||
config.Ruler.EvalDelay = d
|
||||
} else {
|
||||
logger.WarnContext(ctx, "Error parsing RULES_EVAL_DELAY, using default value of 2m")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (config Config) Collect(_ context.Context, _ valuer.UUID) (map[string]any, error) {
|
||||
|
||||
@@ -3,6 +3,8 @@ package signoz
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/authz/signozauthzapi"
|
||||
@@ -65,6 +67,7 @@ type Handlers struct {
|
||||
CloudIntegrationHandler cloudintegration.Handler
|
||||
RuleStateHistory rulestatehistory.Handler
|
||||
AlertmanagerHandler alertmanager.Handler
|
||||
RulerHandler ruler.Handler
|
||||
}
|
||||
|
||||
func NewHandlers(
|
||||
@@ -81,6 +84,7 @@ func NewHandlers(
|
||||
zeusService zeus.Zeus,
|
||||
registryHandler factory.Handler,
|
||||
alertmanagerService alertmanager.Alertmanager,
|
||||
rulerService ruler.Ruler,
|
||||
) Handlers {
|
||||
return Handlers{
|
||||
SavedView: implsavedview.NewHandler(modules.SavedView),
|
||||
@@ -104,5 +108,6 @@ func NewHandlers(
|
||||
RuleStateHistory: implrulestatehistory.NewHandler(modules.RuleStateHistory),
|
||||
CloudIntegrationHandler: implcloudintegration.NewHandler(modules.CloudIntegration),
|
||||
AlertmanagerHandler: signozalertmanager.NewHandler(alertmanagerService),
|
||||
RulerHandler: signozruler.NewHandler(rulerService),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestNewHandlers(t *testing.T) {
|
||||
|
||||
querierHandler := querier.NewHandler(providerSettings, nil, nil)
|
||||
registryHandler := factory.NewHandler(nil)
|
||||
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler, alertmanager)
|
||||
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler, alertmanager, nil)
|
||||
reflectVal := reflect.ValueOf(handlers)
|
||||
for i := 0; i < reflectVal.NumField(); i++ {
|
||||
f := reflectVal.Field(i)
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
@@ -71,6 +72,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ cloudintegration.Handler }{},
|
||||
struct{ rulestatehistory.Handler }{},
|
||||
struct{ alertmanager.Handler }{},
|
||||
struct{ ruler.Handler }{},
|
||||
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -44,9 +44,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/prometheus/clickhouseprometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/querier/signozquerier"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
|
||||
"github.com/SigNoz/signoz/pkg/sharder"
|
||||
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
|
||||
"github.com/SigNoz/signoz/pkg/sharder/singlesharder"
|
||||
@@ -229,11 +226,7 @@ func NewAlertmanagerProviderFactories(sqlstore sqlstore.SQLStore, orgGetter orga
|
||||
)
|
||||
}
|
||||
|
||||
func NewRulerProviderFactories(sqlstore sqlstore.SQLStore, queryParser queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
signozruler.NewFactory(sqlstore, queryParser),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func NewEmailingProviderFactories() factory.NamedMap[factory.ProviderFactory[emailing.Emailing, emailing.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
@@ -289,6 +282,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.CloudIntegrationHandler,
|
||||
handlers.RuleStateHistory,
|
||||
handlers.AlertmanagerHandler,
|
||||
handlers.RulerHandler,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema/sqlschematest"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
@@ -64,11 +63,6 @@ func TestNewProviderFactories(t *testing.T) {
|
||||
NewAlertmanagerProviderFactories(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual), orgGetter, notificationManager)
|
||||
})
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
queryParser := queryparser.New(instrumentationtest.New().ToProviderSettings())
|
||||
NewRulerProviderFactories(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual), queryParser)
|
||||
})
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
NewEmailingProviderFactories()
|
||||
})
|
||||
|
||||
@@ -26,12 +26,14 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount/implserviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/sharder"
|
||||
"github.com/SigNoz/signoz/pkg/sqlmigration"
|
||||
"github.com/SigNoz/signoz/pkg/sqlmigrator"
|
||||
@@ -75,6 +77,7 @@ type SigNoz struct {
|
||||
Tokenizer pkgtokenizer.Tokenizer
|
||||
IdentNResolver identn.IdentNResolver
|
||||
Authz authz.AuthZ
|
||||
Ruler ruler.Ruler
|
||||
Modules Modules
|
||||
Handlers Handlers
|
||||
QueryParser queryparser.QueryParser
|
||||
@@ -103,6 +106,7 @@ func New(
|
||||
auditorProviderFactories func(licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]],
|
||||
querierHandlerCallback func(factory.ProviderSettings, querier.Querier, analytics.Analytics) querier.Handler,
|
||||
cloudIntegrationCallback func(sqlstore.SQLStore, global.Global, zeus.Zeus, gateway.Gateway, licensing.Licensing, serviceaccount.Module, cloudintegration.Config) (cloudintegration.Module, error),
|
||||
rulerProviderFactories func(cache.Cache, alertmanager.Alertmanager, sqlstore.SQLStore, telemetrystore.TelemetryStore, telemetrytypes.MetadataStore, prometheus.Prometheus, organization.Getter, rulestatehistory.Module, querier.Querier, queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]],
|
||||
) (*SigNoz, error) {
|
||||
// Initialize instrumentation
|
||||
instrumentation, err := instrumentation.New(ctx, config.Instrumentation, version.Info, "signoz")
|
||||
@@ -361,18 +365,6 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize ruler from the available ruler provider factories
|
||||
ruler, err := factory.NewProviderFromNamedMap(
|
||||
ctx,
|
||||
providerSettings,
|
||||
config.Ruler,
|
||||
NewRulerProviderFactories(sqlstore, queryParser),
|
||||
"signoz",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gatewayFactory := gatewayProviderFactory(licensing)
|
||||
gateway, err := gatewayFactory.New(ctx, providerSettings, config.Gateway)
|
||||
if err != nil {
|
||||
@@ -441,6 +433,12 @@ func New(
|
||||
// Initialize all modules
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, userRoleStore, serviceAccount, cloudIntegrationModule)
|
||||
|
||||
// Initialize ruler from the variant-specific provider factories
|
||||
rulerInstance, err := factory.NewProviderFromNamedMap(ctx, providerSettings, config.Ruler, rulerProviderFactories(cache, alertmanager, sqlstore, telemetrystore, telemetryMetadataStore, prometheus, orgGetter, modules.RuleStateHistory, querier, queryParser), "signoz")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize identN resolver
|
||||
identNFactories := NewIdentNProviderFactories(tokenizer, serviceAccount, orgGetter, userGetter, config.User)
|
||||
identNResolver, err := identn.NewIdentNResolver(ctx, providerSettings, config.IdentN, identNFactories)
|
||||
@@ -456,7 +454,7 @@ func New(
|
||||
// Create a list of all stats collectors
|
||||
statsCollectors := []statsreporter.StatsCollector{
|
||||
alertmanager,
|
||||
ruler,
|
||||
rulerInstance,
|
||||
modules.Dashboard,
|
||||
modules.SavedView,
|
||||
modules.UserSetter,
|
||||
@@ -493,6 +491,7 @@ func New(
|
||||
factory.NewNamedService(factory.MustNewName("authz"), authz),
|
||||
factory.NewNamedService(factory.MustNewName("user"), userService, factory.MustNewName("authz")),
|
||||
factory.NewNamedService(factory.MustNewName("auditor"), auditor),
|
||||
factory.NewNamedService(factory.MustNewName("ruler"), rulerInstance),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -500,7 +499,7 @@ func New(
|
||||
|
||||
// Initialize all handlers for the modules
|
||||
registryHandler := factory.NewHandler(registry)
|
||||
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager)
|
||||
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager, rulerInstance)
|
||||
|
||||
// Initialize the API server (after registry so it can access service health)
|
||||
apiserverInstance, err := factory.NewProviderFromNamedMap(
|
||||
@@ -534,6 +533,7 @@ func New(
|
||||
Tokenizer: tokenizer,
|
||||
IdentNResolver: identNResolver,
|
||||
Authz: authz,
|
||||
Ruler: rulerInstance,
|
||||
Modules: modules,
|
||||
Handlers: handlers,
|
||||
QueryParser: queryParser,
|
||||
|
||||
@@ -11,19 +11,22 @@ import (
|
||||
alertmanagertemplate "github.com/prometheus/alertmanager/template"
|
||||
)
|
||||
|
||||
func AdditionalFuncMap() tmpltext.FuncMap {
|
||||
return tmpltext.FuncMap{
|
||||
// urlescape escapes the string for use in a URL query parameter.
|
||||
// It returns tmplhtml.HTML to prevent the template engine from escaping the already escaped string.
|
||||
// url.QueryEscape escapes spaces as "+", and html/template escapes "+" as "+" if tmplhtml.HTML is not used.
|
||||
"urlescape": func(value string) tmplhtml.HTML {
|
||||
return tmplhtml.HTML(url.QueryEscape(value))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// customTemplateOption returns an Option that adds custom functions to the template.
|
||||
func customTemplateOption() alertmanagertemplate.Option {
|
||||
return func(text *tmpltext.Template, html *tmplhtml.Template) {
|
||||
funcs := tmpltext.FuncMap{
|
||||
// urlescape escapes the string for use in a URL query parameter.
|
||||
// It returns tmplhtml.HTML to prevent the template engine from escaping the already escaped string.
|
||||
// url.QueryEscape escapes spaces as "+", and html/template escapes "+" as "+" if tmplhtml.HTML is not used.
|
||||
"urlescape": func(value string) tmplhtml.HTML {
|
||||
return tmplhtml.HTML(url.QueryEscape(value))
|
||||
},
|
||||
}
|
||||
text.Funcs(funcs)
|
||||
html.Funcs(funcs)
|
||||
text.Funcs(AdditionalFuncMap())
|
||||
html.Funcs(AdditionalFuncMap())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
144
pkg/types/alertmanagertypes/templatedata.go
Normal file
144
pkg/types/alertmanagertypes/templatedata.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package alertmanagertypes
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
)
|
||||
|
||||
// privateAnnotationPrefix marks annotations the rule manager attaches for
|
||||
// alertmanager-internal use (raw template strings, threshold metadata, link
|
||||
// targets). Annotations whose key starts with this prefix are stripped from
|
||||
// any surface that ends up visible to a template author or a notification
|
||||
// recipient; the alertmanager reads them off the raw alert before stripping.
|
||||
const privateAnnotationPrefix = "_"
|
||||
|
||||
// IsPrivateAnnotation reports whether an annotation key is considered
|
||||
// private — i.e. internal to alertmanager and should not be rendered in
|
||||
// notifications.
|
||||
func IsPrivateAnnotation(key string) bool {
|
||||
return strings.HasPrefix(key, privateAnnotationPrefix)
|
||||
}
|
||||
|
||||
// FilterPublicAnnotations returns a copy of kv with all private-prefixed
|
||||
// keys removed. Callers that expose annotations to templates or notification
|
||||
// payloads should pass them through this first.
|
||||
func FilterPublicAnnotations(kv template.KV) template.KV {
|
||||
out := make(template.KV, len(kv))
|
||||
for k, v := range kv {
|
||||
if IsPrivateAnnotation(k) {
|
||||
continue
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ExpandRequest carries the title/body templates and their defaults handed to
|
||||
// Templater.Expand. Default templates are used when the custom templates
|
||||
// expand to empty strings.
|
||||
type ExpandRequest struct {
|
||||
TitleTemplate string
|
||||
BodyTemplate string
|
||||
DefaultTitleTemplate string
|
||||
DefaultBodyTemplate string
|
||||
}
|
||||
|
||||
// ExpandResult is the rendered output of Templater.Expand.
|
||||
type ExpandResult struct {
|
||||
// Title is the expanded notification title (plain text).
|
||||
Title string
|
||||
// Body is the expanded notification body, one entry per input alert. The
|
||||
// body template is applied per-alert and concatenated by the caller.
|
||||
Body []string
|
||||
// IsDefaultBody is true when Body came from the default template (no
|
||||
// user-authored body was supplied), false when a custom template was used.
|
||||
IsDefaultBody bool
|
||||
// MissingVars is the union of $-references in the title and body templates
|
||||
// that did not resolve to any known field. Surfaced for preview warnings;
|
||||
// at runtime these render as "<no value>".
|
||||
MissingVars []string
|
||||
// NotificationData is the aggregate data that fed the title template,
|
||||
// exposed so callers can reuse it when rendering a channel-specific layout
|
||||
// (e.g. the email HTML shell) without rebuilding it from the alerts.
|
||||
NotificationData *NotificationTemplateData
|
||||
}
|
||||
|
||||
// AlertData holds per-alert data used when expanding body templates.
|
||||
//
|
||||
// Field paths follow OpenTelemetry-style dotted namespaces (via mapstructure
|
||||
// tags) so user templates can reference paths like $alert.is_firing,
|
||||
// $rule.threshold.value, or $log.url. JSON tags use camelCase for the wire
|
||||
// format.
|
||||
type AlertData struct {
|
||||
Alert AlertInfo `json:"alert" mapstructure:"alert"`
|
||||
Rule RuleInfo `json:"rule" mapstructure:"rule"`
|
||||
Log LinkInfo `json:"log" mapstructure:"log"`
|
||||
Trace LinkInfo `json:"trace" mapstructure:"trace"`
|
||||
Labels template.KV `json:"labels" mapstructure:"labels"`
|
||||
Annotations template.KV `json:"annotations" mapstructure:"annotations"`
|
||||
}
|
||||
|
||||
// AlertInfo holds the per-alert state and timing data.
|
||||
type AlertInfo struct {
|
||||
Status string `json:"status" mapstructure:"status"`
|
||||
Receiver string `json:"receiver" mapstructure:"receiver"`
|
||||
Value string `json:"value" mapstructure:"value"`
|
||||
StartsAt time.Time `json:"startsAt" mapstructure:"starts_at"`
|
||||
EndsAt time.Time `json:"endsAt" mapstructure:"ends_at"`
|
||||
GeneratorURL string `json:"generatorURL" mapstructure:"generator_url"`
|
||||
Fingerprint string `json:"fingerprint" mapstructure:"fingerprint"`
|
||||
IsFiring bool `json:"isFiring" mapstructure:"is_firing"`
|
||||
IsResolved bool `json:"isResolved" mapstructure:"is_resolved"`
|
||||
IsMissingData bool `json:"isMissingData" mapstructure:"is_missing_data"`
|
||||
IsRecovering bool `json:"isRecovering" mapstructure:"is_recovering"`
|
||||
}
|
||||
|
||||
// RuleInfo holds the rule metadata extracted from well-known labels and
|
||||
// annotations.
|
||||
type RuleInfo struct {
|
||||
Name string `json:"name" mapstructure:"name"`
|
||||
ID string `json:"id" mapstructure:"id"`
|
||||
URL string `json:"url" mapstructure:"url"`
|
||||
Severity string `json:"severity" mapstructure:"severity"`
|
||||
MatchType string `json:"matchType" mapstructure:"match_type"`
|
||||
Threshold Threshold `json:"threshold" mapstructure:"threshold"`
|
||||
}
|
||||
|
||||
// Threshold describes the breach condition.
|
||||
type Threshold struct {
|
||||
Value string `json:"value" mapstructure:"value"`
|
||||
Op string `json:"op" mapstructure:"op"`
|
||||
}
|
||||
|
||||
// LinkInfo groups a single URL so templates can reference $log.url and
|
||||
// $trace.url uniformly.
|
||||
type LinkInfo struct {
|
||||
URL string `json:"url" mapstructure:"url"`
|
||||
}
|
||||
|
||||
// NotificationTemplateData is the top-level data struct provided to title
|
||||
// templates, representing the aggregate of a grouped notification.
|
||||
type NotificationTemplateData struct {
|
||||
Alert NotificationAlert `json:"alert" mapstructure:"alert"`
|
||||
Rule RuleInfo `json:"rule" mapstructure:"rule"`
|
||||
Labels template.KV `json:"labels" mapstructure:"labels"`
|
||||
Annotations template.KV `json:"annotations" mapstructure:"annotations"`
|
||||
CommonLabels template.KV `json:"commonLabels" mapstructure:"common_labels"`
|
||||
CommonAnnotations template.KV `json:"commonAnnotations" mapstructure:"common_annotations"`
|
||||
GroupLabels template.KV `json:"groupLabels" mapstructure:"group_labels"`
|
||||
ExternalURL string `json:"externalURL" mapstructure:"external_url"`
|
||||
|
||||
// Per-alert data kept for body expansion; not exposed to the title template.
|
||||
Alerts []AlertData `json:"-" mapstructure:"-"`
|
||||
}
|
||||
|
||||
// NotificationAlert holds the aggregate alert fields available to title
|
||||
// templates (counts, overall status, receiver).
|
||||
type NotificationAlert struct {
|
||||
Receiver string `json:"receiver" mapstructure:"receiver"`
|
||||
Status string `json:"status" mapstructure:"status"`
|
||||
TotalFiring int `json:"totalFiring" mapstructure:"total_firing"`
|
||||
TotalResolved int `json:"totalResolved" mapstructure:"total_resolved"`
|
||||
}
|
||||
@@ -71,6 +71,15 @@ var (
|
||||
PanelTypeGraph = PanelType{valuer.NewString("graph")}
|
||||
)
|
||||
|
||||
// Enum implements jsonschema.Enum; returns the acceptable values for PanelType.
|
||||
func (PanelType) Enum() []any {
|
||||
return []any{
|
||||
PanelTypeValue,
|
||||
PanelTypeTable,
|
||||
PanelTypeGraph,
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this is used to represent the state of the alert query
|
||||
// i.e the active tab which should be used to represent the selection
|
||||
|
||||
@@ -84,23 +93,32 @@ var (
|
||||
QueryTypePromQL = QueryType{valuer.NewString("promql")}
|
||||
)
|
||||
|
||||
type AlertCompositeQuery struct {
|
||||
Queries []qbtypes.QueryEnvelope `json:"queries"`
|
||||
// Enum implements jsonschema.Enum; returns the acceptable values for QueryType.
|
||||
func (QueryType) Enum() []any {
|
||||
return []any{
|
||||
QueryTypeBuilder,
|
||||
QueryTypeClickHouseSQL,
|
||||
QueryTypePromQL,
|
||||
}
|
||||
}
|
||||
|
||||
PanelType PanelType `json:"panelType"`
|
||||
QueryType QueryType `json:"queryType"`
|
||||
type AlertCompositeQuery struct {
|
||||
Queries []qbtypes.QueryEnvelope `json:"queries" required:"true"`
|
||||
|
||||
PanelType PanelType `json:"panelType" required:"true"`
|
||||
QueryType QueryType `json:"queryType" required:"true"`
|
||||
// Unit for the time series data shown in the graph
|
||||
// This is used to format the value and threshold
|
||||
Unit string `json:"unit,omitempty"`
|
||||
}
|
||||
|
||||
type RuleCondition struct {
|
||||
CompositeQuery *AlertCompositeQuery `json:"compositeQuery"`
|
||||
CompareOperator CompareOperator `json:"op"`
|
||||
CompositeQuery *AlertCompositeQuery `json:"compositeQuery" required:"true"`
|
||||
CompareOperator CompareOperator `json:"op" required:"true"`
|
||||
Target *float64 `json:"target,omitempty"`
|
||||
AlertOnAbsent bool `json:"alertOnAbsent,omitempty"`
|
||||
AbsentFor uint64 `json:"absentFor,omitempty"`
|
||||
MatchType MatchType `json:"matchType"`
|
||||
MatchType MatchType `json:"matchType" required:"true"`
|
||||
TargetUnit string `json:"targetUnit,omitempty"`
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
Seasonality Seasonality `json:"seasonality,omitzero"`
|
||||
|
||||
@@ -38,14 +38,14 @@ const (
|
||||
|
||||
// PostableRule is used to create alerting rule from HTTP api.
|
||||
type PostableRule struct {
|
||||
AlertName string `json:"alert"`
|
||||
AlertName string `json:"alert" required:"true"`
|
||||
AlertType AlertType `json:"alertType,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
RuleType RuleType `json:"ruleType,omitzero"`
|
||||
RuleType RuleType `json:"ruleType,omitzero" required:"true"`
|
||||
EvalWindow valuer.TextDuration `json:"evalWindow,omitzero"`
|
||||
Frequency valuer.TextDuration `json:"frequency,omitzero"`
|
||||
|
||||
RuleCondition *RuleCondition `json:"condition,omitempty"`
|
||||
RuleCondition *RuleCondition `json:"condition,omitempty" required:"true"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
|
||||
@@ -586,6 +586,11 @@ func testTemplateParsing(rl *PostableRule) (errs []error) {
|
||||
}
|
||||
|
||||
// GettableRules has info for all stored rules.
|
||||
type GettableTestRule struct {
|
||||
AlertCount int `json:"alertCount"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type GettableRules struct {
|
||||
Rules []*GettableRule `json:"rules"`
|
||||
}
|
||||
|
||||
@@ -9,4 +9,33 @@ const (
|
||||
LabelSeverityName = "severity"
|
||||
LabelLastSeen = "lastSeen"
|
||||
LabelRuleID = "ruleId"
|
||||
LabelRuleSource = "ruleSource"
|
||||
LabelNoData = "nodata"
|
||||
LabelTestAlert = "testalert"
|
||||
LabelAlertName = "alertname"
|
||||
LabelIsRecovering = "is_recovering"
|
||||
)
|
||||
|
||||
// Annotations set by the rule manager and consumed by the alertmanager
|
||||
// templating layer.
|
||||
//
|
||||
// Those prefixed with "_" are private: they're stripped from
|
||||
// notification-visible surfaces by alertmanagertypes.FilterPublicAnnotations
|
||||
// before rendering. Only the raw template strings are private — echoing
|
||||
// them into a notification is circular and never useful.
|
||||
//
|
||||
// The rest are public: they describe the firing alert (the breached value,
|
||||
// the configured threshold, the comparator, the match type, and deep links
|
||||
// to relevant logs/traces) and users may reference them directly as
|
||||
// {{ .Annotations.value }}, {{ .Annotations.threshold.value }}, etc. in
|
||||
// their channel templates.
|
||||
const (
|
||||
AnnotationTitleTemplate = "_title_template"
|
||||
AnnotationBodyTemplate = "_body_template"
|
||||
AnnotationRelatedLogs = "related_logs"
|
||||
AnnotationRelatedTraces = "related_traces"
|
||||
AnnotationValue = "value"
|
||||
AnnotationThresholdValue = "threshold.value"
|
||||
AnnotationCompareOp = "compare_op"
|
||||
AnnotationMatchType = "match_type"
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type EvaluationKind struct {
|
||||
@@ -17,14 +18,22 @@ var (
|
||||
CumulativeEvaluation = EvaluationKind{valuer.NewString("cumulative")}
|
||||
)
|
||||
|
||||
// Enum implements jsonschema.Enum; returns the acceptable values for EvaluationKind.
|
||||
func (EvaluationKind) Enum() []any {
|
||||
return []any{
|
||||
RollingEvaluation,
|
||||
CumulativeEvaluation,
|
||||
}
|
||||
}
|
||||
|
||||
type Evaluation interface {
|
||||
NextWindowFor(curr time.Time) (time.Time, time.Time)
|
||||
GetFrequency() valuer.TextDuration
|
||||
}
|
||||
|
||||
type RollingWindow struct {
|
||||
EvalWindow valuer.TextDuration `json:"evalWindow"`
|
||||
Frequency valuer.TextDuration `json:"frequency"`
|
||||
EvalWindow valuer.TextDuration `json:"evalWindow" required:"true"`
|
||||
Frequency valuer.TextDuration `json:"frequency" required:"true"`
|
||||
}
|
||||
|
||||
func (rollingWindow RollingWindow) Validate() error {
|
||||
@@ -46,9 +55,9 @@ func (rollingWindow RollingWindow) GetFrequency() valuer.TextDuration {
|
||||
}
|
||||
|
||||
type CumulativeWindow struct {
|
||||
Schedule CumulativeSchedule `json:"schedule"`
|
||||
Frequency valuer.TextDuration `json:"frequency"`
|
||||
Timezone string `json:"timezone"`
|
||||
Schedule CumulativeSchedule `json:"schedule" required:"true"`
|
||||
Frequency valuer.TextDuration `json:"frequency" required:"true"`
|
||||
Timezone string `json:"timezone" required:"true"`
|
||||
}
|
||||
|
||||
type CumulativeSchedule struct {
|
||||
@@ -70,6 +79,16 @@ var (
|
||||
ScheduleTypeMonthly = ScheduleType{valuer.NewString("monthly")}
|
||||
)
|
||||
|
||||
// Enum implements jsonschema.Enum; returns the acceptable values for ScheduleType.
|
||||
func (ScheduleType) Enum() []any {
|
||||
return []any{
|
||||
ScheduleTypeHourly,
|
||||
ScheduleTypeDaily,
|
||||
ScheduleTypeWeekly,
|
||||
ScheduleTypeMonthly,
|
||||
}
|
||||
}
|
||||
|
||||
func (cumulativeWindow CumulativeWindow) Validate() error {
|
||||
// Validate schedule
|
||||
if err := cumulativeWindow.Schedule.Validate(); err != nil {
|
||||
@@ -225,8 +244,31 @@ func (cumulativeWindow CumulativeWindow) GetFrequency() valuer.TextDuration {
|
||||
}
|
||||
|
||||
type EvaluationEnvelope struct {
|
||||
Kind EvaluationKind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
Kind EvaluationKind `json:"kind" required:"true"`
|
||||
Spec any `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
// evaluationRolling is the OpenAPI schema for an EvaluationEnvelope with kind=rolling.
|
||||
type evaluationRolling struct {
|
||||
Kind EvaluationKind `json:"kind" description:"The kind of evaluation."`
|
||||
Spec RollingWindow `json:"spec" description:"The rolling window evaluation specification."`
|
||||
}
|
||||
|
||||
// evaluationCumulative is the OpenAPI schema for an EvaluationEnvelope with kind=cumulative.
|
||||
type evaluationCumulative struct {
|
||||
Kind EvaluationKind `json:"kind" description:"The kind of evaluation."`
|
||||
Spec CumulativeWindow `json:"spec" description:"The cumulative window evaluation specification."`
|
||||
}
|
||||
|
||||
var _ jsonschema.OneOfExposer = EvaluationEnvelope{}
|
||||
|
||||
// JSONSchemaOneOf returns the oneOf variants for the EvaluationEnvelope discriminated union.
|
||||
// Each variant represents a different evaluation kind with its corresponding spec schema.
|
||||
func (EvaluationEnvelope) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
evaluationRolling{},
|
||||
evaluationCumulative{},
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EvaluationEnvelope) UnmarshalJSON(data []byte) error {
|
||||
|
||||
@@ -28,9 +28,9 @@ type StorablePlannedMaintenance struct {
|
||||
|
||||
type GettablePlannedMaintenance struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name" required:"true"`
|
||||
Description string `json:"description"`
|
||||
Schedule *Schedule `json:"schedule"`
|
||||
Schedule *Schedule `json:"schedule" required:"true"`
|
||||
RuleIDs []string `json:"alertIds"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
@@ -331,6 +331,11 @@ func (m *GettablePlannedMaintenanceRule) ConvertGettableMaintenanceRuleToGettabl
|
||||
}
|
||||
}
|
||||
|
||||
type ListPlannedMaintenanceParams struct {
|
||||
Active *bool `query:"active"`
|
||||
Recurring *bool `query:"recurring"`
|
||||
}
|
||||
|
||||
type MaintenanceStore interface {
|
||||
CreatePlannedMaintenance(context.Context, GettablePlannedMaintenance) (valuer.UUID, error)
|
||||
DeletePlannedMaintenance(context.Context, valuer.UUID) error
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
type Schedule struct {
|
||||
Timezone string `json:"timezone"`
|
||||
Timezone string `json:"timezone" required:"true"`
|
||||
StartTime time.Time `json:"startTime,omitempty"`
|
||||
EndTime time.Time `json:"endTime,omitempty"`
|
||||
Recurrence *Recurrence `json:"recurrence"`
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/units"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type ThresholdKind struct {
|
||||
@@ -21,9 +22,32 @@ var (
|
||||
BasicThresholdKind = ThresholdKind{valuer.NewString("basic")}
|
||||
)
|
||||
|
||||
// Enum implements jsonschema.Enum; returns the acceptable values for ThresholdKind.
|
||||
func (ThresholdKind) Enum() []any {
|
||||
return []any{
|
||||
BasicThresholdKind,
|
||||
}
|
||||
}
|
||||
|
||||
type RuleThresholdData struct {
|
||||
Kind ThresholdKind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
Kind ThresholdKind `json:"kind" required:"true"`
|
||||
Spec any `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
// thresholdBasic is the OpenAPI schema for a RuleThresholdData with kind=basic.
|
||||
type thresholdBasic struct {
|
||||
Kind ThresholdKind `json:"kind" description:"The kind of threshold."`
|
||||
Spec BasicRuleThresholds `json:"spec" description:"The basic threshold specification (array of thresholds)."`
|
||||
}
|
||||
|
||||
var _ jsonschema.OneOfExposer = RuleThresholdData{}
|
||||
|
||||
// JSONSchemaOneOf returns the oneOf variants for the RuleThresholdData discriminated union.
|
||||
// Each variant represents a different threshold kind with its corresponding spec schema.
|
||||
func (RuleThresholdData) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
thresholdBasic{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RuleThresholdData) UnmarshalJSON(data []byte) error {
|
||||
@@ -88,12 +112,12 @@ type RuleThreshold interface {
|
||||
}
|
||||
|
||||
type BasicRuleThreshold struct {
|
||||
Name string `json:"name"`
|
||||
TargetValue *float64 `json:"target"`
|
||||
Name string `json:"name" required:"true"`
|
||||
TargetValue *float64 `json:"target" required:"true"`
|
||||
TargetUnit string `json:"targetUnit"`
|
||||
RecoveryTarget *float64 `json:"recoveryTarget"`
|
||||
MatchType MatchType `json:"matchType"`
|
||||
CompareOperator CompareOperator `json:"op"`
|
||||
MatchType MatchType `json:"matchType" required:"true"`
|
||||
CompareOperator CompareOperator `json:"op" required:"true"`
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user