mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-20 02:30:30 +01:00
Compare commits
10 Commits
feat/markd
...
feat/cloud
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b0d5dcf0e | ||
|
|
aa8c4471dc | ||
|
|
04b8ef4d86 | ||
|
|
9aee83607f | ||
|
|
61ae49d4ab | ||
|
|
a5e1f71cf6 | ||
|
|
c9610df66d | ||
|
|
ef298af388 | ||
|
|
b5c146afdf | ||
|
|
d8abbce47e |
@@ -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"
|
||||
@@ -75,7 +83,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
},
|
||||
signoz.NewEmailingProviderFactories(),
|
||||
signoz.NewCacheProviderFactories(),
|
||||
signoz.NewWebProviderFactories(),
|
||||
signoz.NewWebProviderFactories(config.Global),
|
||||
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
|
||||
return signoz.NewSQLSchemaProviderFactories(sqlstore)
|
||||
},
|
||||
@@ -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"
|
||||
)
|
||||
@@ -96,7 +105,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
},
|
||||
signoz.NewEmailingProviderFactories(),
|
||||
signoz.NewCacheProviderFactories(),
|
||||
signoz.NewWebProviderFactories(),
|
||||
signoz.NewWebProviderFactories(config.Global),
|
||||
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
|
||||
existingFactories := signoz.NewSQLSchemaProviderFactories(sqlstore)
|
||||
if err := existingFactories.Add(postgressqlschema.NewFactory(sqlstore)); err != nil {
|
||||
@@ -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))
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
##################### Global #####################
|
||||
global:
|
||||
# the url under which the signoz apiserver is externally reachable.
|
||||
# the path component (e.g. /signoz in https://example.com/signoz) is used
|
||||
# as the base path for all HTTP routes (both API and web frontend).
|
||||
external_url: <unset>
|
||||
# the url where the SigNoz backend receives telemetry data (traces, metrics, logs) from instrumented applications.
|
||||
ingestion_url: <unset>
|
||||
@@ -50,8 +52,8 @@ pprof:
|
||||
web:
|
||||
# Whether to enable the web frontend
|
||||
enabled: true
|
||||
# The prefix to serve web on
|
||||
prefix: /
|
||||
# The index file to use as the SPA entrypoint.
|
||||
index: index.html
|
||||
# The directory containing the static build files.
|
||||
directory: /etc/signoz/web
|
||||
|
||||
|
||||
1252
docs/api/openapi.yml
1252
docs/api/openapi.yml
File diff suppressed because it is too large
Load Diff
@@ -19,11 +19,11 @@ func NewAWSCloudProvider(defStore cloudintegrationtypes.ServiceDefinitionStore)
|
||||
}
|
||||
|
||||
func (provider *awscloudprovider) GetConnectionArtifact(ctx context.Context, account *cloudintegrationtypes.Account, req *cloudintegrationtypes.GetConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
|
||||
baseURL := fmt.Sprintf(cloudintegrationtypes.CloudFormationQuickCreateBaseURL.StringValue(), req.Config.Aws.DeploymentRegion)
|
||||
baseURL := fmt.Sprintf(cloudintegrationtypes.CloudFormationQuickCreateBaseURL.StringValue(), req.Config.AWS.DeploymentRegion)
|
||||
u, _ := url.Parse(baseURL)
|
||||
|
||||
q := u.Query()
|
||||
q.Set("region", req.Config.Aws.DeploymentRegion)
|
||||
q.Set("region", req.Config.AWS.DeploymentRegion)
|
||||
u.Fragment = "/stacks/quickcreate"
|
||||
|
||||
u.RawQuery = q.Encode()
|
||||
@@ -39,9 +39,7 @@ func (provider *awscloudprovider) GetConnectionArtifact(ctx context.Context, acc
|
||||
q.Set("param_IngestionKey", req.Credentials.IngestionKey)
|
||||
|
||||
return &cloudintegrationtypes.ConnectionArtifact{
|
||||
Aws: &cloudintegrationtypes.AWSConnectionArtifact{
|
||||
ConnectionURL: u.String() + "?&" + q.Encode(), // this format is required by AWS
|
||||
},
|
||||
AWS: cloudintegrationtypes.NewAWSConnectionArtifact(u.String() + "?&" + q.Encode()), // this format is required by AWS
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -124,9 +122,6 @@ func (provider *awscloudprovider) BuildIntegrationConfig(
|
||||
}
|
||||
|
||||
return &cloudintegrationtypes.ProviderIntegrationConfig{
|
||||
AWS: &cloudintegrationtypes.AWSIntegrationConfig{
|
||||
EnabledRegions: account.Config.AWS.Regions,
|
||||
TelemetryCollectionStrategy: collectionStrategy,
|
||||
},
|
||||
AWS: cloudintegrationtypes.NewAWSIntegrationConfig(account.Config.AWS.Regions, collectionStrategy),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -262,6 +228,20 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routePrefix := s.config.Global.ExternalPath()
|
||||
if routePrefix != "" {
|
||||
prefixed := http.StripPrefix(routePrefix, handler)
|
||||
handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
switch req.URL.Path {
|
||||
case "/api/v1/health", "/api/v2/healthz", "/api/v2/readyz", "/api/v2/livez":
|
||||
r.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
prefixed.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
return &http.Server{
|
||||
Handler: handler,
|
||||
}, nil
|
||||
@@ -288,8 +268,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 +311,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",
|
||||
|
||||
496
frontend/src/api/generated/services/downtimeschedules/index.ts
Normal file
496
frontend/src/api/generated/services/downtimeschedules/index.ts
Normal file
@@ -0,0 +1,496 @@
|
||||
/**
|
||||
* ! 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 {
|
||||
CreateDowntimeSchedule201,
|
||||
DeleteDowntimeScheduleByIDPathParameters,
|
||||
GetDowntimeScheduleByID200,
|
||||
GetDowntimeScheduleByIDPathParameters,
|
||||
ListDowntimeSchedules200,
|
||||
ListDowntimeSchedulesParams,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesPostablePlannedMaintenanceDTO,
|
||||
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 = (
|
||||
ruletypesPostablePlannedMaintenanceDTO: BodyType<RuletypesPostablePlannedMaintenanceDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateDowntimeSchedule201>({
|
||||
url: `/api/v1/downtime_schedules`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: ruletypesPostablePlannedMaintenanceDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateDowntimeScheduleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
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<RuletypesPostablePlannedMaintenanceDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createDowntimeSchedule(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateDowntimeScheduleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>
|
||||
>;
|
||||
export type CreateDowntimeScheduleMutationBody = BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
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<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
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,
|
||||
ruletypesPostablePlannedMaintenanceDTO: BodyType<RuletypesPostablePlannedMaintenanceDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/downtime_schedules/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: ruletypesPostablePlannedMaintenanceDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateDowntimeScheduleByIDMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
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<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateDowntimeScheduleByID(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateDowntimeScheduleByIDMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>
|
||||
>;
|
||||
export type UpdateDowntimeScheduleByIDMutationBody = BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
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<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -795,7 +795,8 @@ export interface CloudintegrationtypesAccountDTO {
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesAccountConfigDTO {
|
||||
aws: CloudintegrationtypesAWSAccountConfigDTO;
|
||||
aws?: CloudintegrationtypesAWSAccountConfigDTO;
|
||||
azure?: CloudintegrationtypesAzureAccountConfigDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -829,6 +830,66 @@ export interface CloudintegrationtypesAssetsDTO {
|
||||
dashboards?: CloudintegrationtypesDashboardDTO[] | null;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesAzureAccountConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
deploymentRegion: string;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
resourceGroups: string[];
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesAzureConnectionArtifactDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
cliCommand: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
cloudShellCommand: string;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesAzureLogsCollectionStrategyDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
categories?: string[];
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesAzureMetricsCollectionStrategyDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
serviceNames: string[];
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesAzureServiceConfigDTO {
|
||||
logs?: CloudintegrationtypesAzureServiceLogsConfigDTO;
|
||||
metrics?: CloudintegrationtypesAzureServiceMetricsConfigDTO;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesAzureServiceLogsConfigDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesAzureServiceMetricsConfigDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesAzureTelemetryCollectionStrategyDTO {
|
||||
logs?: CloudintegrationtypesAzureLogsCollectionStrategyDTO;
|
||||
metrics?: CloudintegrationtypesAzureMetricsCollectionStrategyDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
@@ -890,7 +951,8 @@ export interface CloudintegrationtypesCollectedMetricDTO {
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesConnectionArtifactDTO {
|
||||
aws: CloudintegrationtypesAWSConnectionArtifactDTO;
|
||||
aws?: CloudintegrationtypesAWSConnectionArtifactDTO;
|
||||
azure?: CloudintegrationtypesAzureConnectionArtifactDTO;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesCredentialsDTO {
|
||||
@@ -1072,7 +1134,8 @@ export interface CloudintegrationtypesPostableAccountDTO {
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesPostableAccountConfigDTO {
|
||||
aws: CloudintegrationtypesAWSPostableAccountConfigDTO;
|
||||
aws?: CloudintegrationtypesAWSPostableAccountConfigDTO;
|
||||
azure?: CloudintegrationtypesAzureAccountConfigDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1135,7 +1198,8 @@ export interface CloudintegrationtypesServiceDTO {
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesServiceConfigDTO {
|
||||
aws: CloudintegrationtypesAWSServiceConfigDTO;
|
||||
aws?: CloudintegrationtypesAWSServiceConfigDTO;
|
||||
azure?: CloudintegrationtypesAzureServiceConfigDTO;
|
||||
}
|
||||
|
||||
export enum CloudintegrationtypesServiceIDDTO {
|
||||
@@ -1184,11 +1248,24 @@ export interface CloudintegrationtypesSupportedSignalsDTO {
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesTelemetryCollectionStrategyDTO {
|
||||
aws: CloudintegrationtypesAWSTelemetryCollectionStrategyDTO;
|
||||
aws?: CloudintegrationtypesAWSTelemetryCollectionStrategyDTO;
|
||||
azure?: CloudintegrationtypesAzureTelemetryCollectionStrategyDTO;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesUpdatableAccountDTO {
|
||||
config: CloudintegrationtypesAccountConfigDTO;
|
||||
config: CloudintegrationtypesUpdatableAccountConfigDTO;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesUpdatableAccountConfigDTO {
|
||||
aws?: CloudintegrationtypesAWSAccountConfigDTO;
|
||||
azure?: CloudintegrationtypesUpdatableAzureAccountConfigDTO;
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesUpdatableAzureAccountConfigDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
resourceGroups: string[];
|
||||
}
|
||||
|
||||
export interface CloudintegrationtypesUpdatableServiceDTO {
|
||||
@@ -4529,6 +4606,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 +4628,513 @@ export enum RuletypesAlertStateDTO {
|
||||
nodata = 'nodata',
|
||||
disabled = 'disabled',
|
||||
}
|
||||
export enum RuletypesAlertTypeDTO {
|
||||
METRIC_BASED_ALERT = 'METRIC_BASED_ALERT',
|
||||
TRACES_BASED_ALERT = 'TRACES_BASED_ALERT',
|
||||
LOGS_BASED_ALERT = 'LOGS_BASED_ALERT',
|
||||
EXCEPTIONS_BASED_ALERT = 'EXCEPTIONS_BASED_ALERT',
|
||||
}
|
||||
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 RuletypesGettableTestRuleDTO {
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
alertCount?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export enum RuletypesMaintenanceKindDTO {
|
||||
fixed = 'fixed',
|
||||
recurring = 'recurring',
|
||||
}
|
||||
export enum RuletypesMaintenanceStatusDTO {
|
||||
active = 'active',
|
||||
upcoming = 'upcoming',
|
||||
expired = 'expired',
|
||||
}
|
||||
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 interface RuletypesPlannedMaintenanceDTO {
|
||||
/**
|
||||
* @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;
|
||||
kind: RuletypesMaintenanceKindDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
schedule: RuletypesScheduleDTO;
|
||||
status: RuletypesMaintenanceStatusDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface RuletypesPostablePlannedMaintenanceDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
alertIds?: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
schedule: RuletypesScheduleDTO;
|
||||
}
|
||||
|
||||
export type RuletypesPostableRuleDTOAnnotations = { [key: string]: string };
|
||||
|
||||
export type RuletypesPostableRuleDTOLabels = { [key: string]: string };
|
||||
|
||||
export interface RuletypesPostableRuleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
alert: string;
|
||||
alertType?: RuletypesAlertTypeDTO;
|
||||
/**
|
||||
* @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?: RuletypesRepeatOnDTO[] | null;
|
||||
repeatType: RuletypesRepeatTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
startTime: Date;
|
||||
}
|
||||
|
||||
export interface RuletypesRenotifyDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
alertStates?: RuletypesAlertStateDTO[];
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
interval?: string;
|
||||
}
|
||||
|
||||
export enum RuletypesRepeatOnDTO {
|
||||
sunday = 'sunday',
|
||||
monday = 'monday',
|
||||
tuesday = 'tuesday',
|
||||
wednesday = 'wednesday',
|
||||
thursday = 'thursday',
|
||||
friday = 'friday',
|
||||
saturday = 'saturday',
|
||||
}
|
||||
export enum RuletypesRepeatTypeDTO {
|
||||
daily = 'daily',
|
||||
weekly = 'weekly',
|
||||
monthly = 'monthly',
|
||||
}
|
||||
export interface RuletypesRollingWindowDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
evalWindow: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
frequency: string;
|
||||
}
|
||||
|
||||
export type RuletypesRuleDTOAnnotations = { [key: string]: string };
|
||||
|
||||
export type RuletypesRuleDTOLabels = { [key: string]: string };
|
||||
|
||||
export interface RuletypesRuleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
alert: string;
|
||||
alertType?: RuletypesAlertTypeDTO;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
annotations?: RuletypesRuleDTOAnnotations;
|
||||
condition: RuletypesRuleConditionDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @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?: RuletypesRuleDTOLabels;
|
||||
notificationSettings?: RuletypesNotificationSettingsDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
preferredChannels?: string[];
|
||||
ruleType: RuletypesRuleTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
source?: string;
|
||||
state: RuletypesAlertStateDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
version?: 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 +6054,57 @@ 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: RuletypesPlannedMaintenanceDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateDowntimeSchedule201 = {
|
||||
data: RuletypesPlannedMaintenanceDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteDowntimeScheduleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetDowntimeScheduleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetDowntimeScheduleByID200 = {
|
||||
data: RuletypesPlannedMaintenanceDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateDowntimeScheduleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type HandleExportRawDataPOSTParams = {
|
||||
/**
|
||||
* @enum csv,jsonl
|
||||
@@ -6253,6 +6902,53 @@ export type GetUsersByRoleID200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListRules200 = {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
data: RuletypesRuleDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateRule201 = {
|
||||
data: RuletypesRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteRuleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetRuleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetRuleByID200 = {
|
||||
data: RuletypesRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type PatchRuleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type PatchRuleByID200 = {
|
||||
data: RuletypesRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateRuleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetRuleHistoryFilterKeysPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
@@ -6523,6 +7219,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"
|
||||
|
||||
1
go.mod
1
go.mod
@@ -64,7 +64,6 @@ require (
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.9
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
|
||||
github.com/uptrace/bun/extra/bunotel v1.2.9
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
go.opentelemetry.io/collector/confmap v1.51.0
|
||||
go.opentelemetry.io/collector/otelcol v0.144.0
|
||||
go.opentelemetry.io/collector/pdata v1.51.0
|
||||
|
||||
2
go.sum
2
go.sum
@@ -1144,8 +1144,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zeebo/assert v1.3.1 h1:vukIABvugfNMZMQO1ABsyQDJDTVQbn+LWSMy1ol1h6A=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
185
pkg/apiserver/signozapiserver/ruler.go
Normal file
185
pkg/apiserver/signozapiserver/ruler.go
Normal file
@@ -0,0 +1,185 @@
|
||||
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: make([]*ruletypes.Rule, 0),
|
||||
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.Rule),
|
||||
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.Rule),
|
||||
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.Rule),
|
||||
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.PlannedMaintenance, 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.PlannedMaintenance),
|
||||
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.PostablePlannedMaintenance),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(ruletypes.PlannedMaintenance),
|
||||
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/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.PostablePlannedMaintenance),
|
||||
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
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package global
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
|
||||
@@ -37,5 +39,34 @@ func newConfig() factory.Config {
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if c.ExternalURL != nil {
|
||||
if c.ExternalURL.Path != "" && c.ExternalURL.Path != "/" {
|
||||
if !strings.HasPrefix(c.ExternalURL.Path, "/") {
|
||||
return errors.NewInvalidInputf(ErrCodeInvalidGlobalConfig, "global::external_url path must start with '/', got %q", c.ExternalURL.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Config) ExternalPath() string {
|
||||
if c.ExternalURL == nil || c.ExternalURL.Path == "" || c.ExternalURL.Path == "/" {
|
||||
return ""
|
||||
}
|
||||
|
||||
p := path.Clean("/" + c.ExternalURL.Path)
|
||||
if p == "/" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (c Config) ExternalPathTrailing() string {
|
||||
if p := c.ExternalPath(); p != "" {
|
||||
return p + "/"
|
||||
}
|
||||
|
||||
return "/"
|
||||
}
|
||||
|
||||
139
pkg/global/config_test.go
Normal file
139
pkg/global/config_test.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExternalPath(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
config Config
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "NilURL",
|
||||
config: Config{ExternalURL: nil},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "EmptyPath",
|
||||
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: ""}},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "RootPath",
|
||||
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/"}},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "SingleSegment",
|
||||
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
|
||||
expected: "/signoz",
|
||||
},
|
||||
{
|
||||
name: "TrailingSlash",
|
||||
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz/"}},
|
||||
expected: "/signoz",
|
||||
},
|
||||
{
|
||||
name: "MultiSegment",
|
||||
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/a/b/c"}},
|
||||
expected: "/a/b/c",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expected, tc.config.ExternalPath())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalPathTrailing(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
config Config
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "NilURL",
|
||||
config: Config{ExternalURL: nil},
|
||||
expected: "/",
|
||||
},
|
||||
{
|
||||
name: "EmptyPath",
|
||||
config: Config{ExternalURL: &url.URL{Path: ""}},
|
||||
expected: "/",
|
||||
},
|
||||
{
|
||||
name: "RootPath",
|
||||
config: Config{ExternalURL: &url.URL{Path: "/"}},
|
||||
expected: "/",
|
||||
},
|
||||
{
|
||||
name: "SingleSegment",
|
||||
config: Config{ExternalURL: &url.URL{Path: "/signoz"}},
|
||||
expected: "/signoz/",
|
||||
},
|
||||
{
|
||||
name: "MultiSegment",
|
||||
config: Config{ExternalURL: &url.URL{Path: "/a/b/c"}},
|
||||
expected: "/a/b/c/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expected, tc.config.ExternalPathTrailing())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
config Config
|
||||
fail bool
|
||||
}{
|
||||
{
|
||||
name: "NilURL",
|
||||
config: Config{ExternalURL: nil},
|
||||
fail: false,
|
||||
},
|
||||
{
|
||||
name: "EmptyPath",
|
||||
config: Config{ExternalURL: &url.URL{Path: ""}},
|
||||
fail: false,
|
||||
},
|
||||
{
|
||||
name: "RootPath",
|
||||
config: Config{ExternalURL: &url.URL{Path: "/"}},
|
||||
fail: false,
|
||||
},
|
||||
{
|
||||
name: "ValidPath",
|
||||
config: Config{ExternalURL: &url.URL{Path: "/signoz"}},
|
||||
fail: false,
|
||||
},
|
||||
{
|
||||
name: "NoLeadingSlash",
|
||||
config: Config{ExternalURL: &url.URL{Path: "signoz"}},
|
||||
fail: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.config.Validate()
|
||||
if tc.fail {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -587,7 +578,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
router.HandleFunc("/api/v1/query_filter/analyze", am.ViewAccess(aH.QueryParserAPI.AnalyzeQueryFilter)).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
|
||||
func Intersection(a, b []int) (c []int) {
|
||||
m := make(map[int]bool)
|
||||
|
||||
@@ -603,26 +593,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 +648,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 +1019,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 +1220,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),
|
||||
}
|
||||
@@ -244,6 +211,20 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routePrefix := s.config.Global.ExternalPath()
|
||||
if routePrefix != "" {
|
||||
prefixed := http.StripPrefix(routePrefix, handler)
|
||||
handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
switch req.URL.Path {
|
||||
case "/api/v1/health", "/api/v2/healthz", "/api/v2/readyz", "/api/v2/livez":
|
||||
r.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
prefixed.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
return &http.Server{
|
||||
Handler: handler,
|
||||
}, nil
|
||||
@@ -270,8 +251,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 +294,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
|
||||
}
|
||||
|
||||
@@ -577,7 +577,7 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*ruletypes.Ge
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
storedRule := &ruletypes.Rule{
|
||||
storedRule := &ruletypes.StorableRule{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
@@ -856,9 +856,9 @@ func (m *Manager) ListRuleStates(ctx context.Context) (*ruletypes.GettableRules,
|
||||
} else {
|
||||
ruleResponse.State = rm.State()
|
||||
}
|
||||
ruleResponse.CreatedAt = &s.CreatedAt
|
||||
ruleResponse.CreatedAt = s.CreatedAt
|
||||
ruleResponse.CreatedBy = &s.CreatedBy
|
||||
ruleResponse.UpdatedAt = &s.UpdatedAt
|
||||
ruleResponse.UpdatedAt = s.UpdatedAt
|
||||
ruleResponse.UpdatedBy = &s.UpdatedBy
|
||||
resp = append(resp, &ruleResponse)
|
||||
}
|
||||
@@ -885,9 +885,9 @@ func (m *Manager) GetRule(ctx context.Context, id valuer.UUID) (*ruletypes.Getta
|
||||
} else {
|
||||
r.State = rm.State()
|
||||
}
|
||||
r.CreatedAt = &s.CreatedAt
|
||||
r.CreatedAt = s.CreatedAt
|
||||
r.CreatedBy = &s.CreatedBy
|
||||
r.UpdatedAt = &s.UpdatedAt
|
||||
r.UpdatedAt = s.UpdatedAt
|
||||
r.UpdatedBy = &s.UpdatedBy
|
||||
|
||||
return &r, nil
|
||||
|
||||
@@ -330,7 +330,7 @@ func (g *PromRuleTask) Eval(ctx context.Context, ts time.Time) {
|
||||
}()
|
||||
|
||||
g.logger.InfoContext(ctx, "promql rule task", "name", g.name, "eval_started_at", ts)
|
||||
maintenance, err := g.maintenanceStore.GetAllPlannedMaintenance(ctx, g.orgID.StringValue())
|
||||
maintenance, err := g.maintenanceStore.ListPlannedMaintenance(ctx, g.orgID.StringValue())
|
||||
if err != nil {
|
||||
g.logger.ErrorContext(ctx, "error in processing sql query", errors.Attr(err))
|
||||
}
|
||||
|
||||
@@ -318,7 +318,7 @@ func (g *RuleTask) Eval(ctx context.Context, ts time.Time) {
|
||||
|
||||
g.logger.DebugContext(ctx, "rule task eval started", "name", g.name, "start_time", ts)
|
||||
|
||||
maintenance, err := g.maintenanceStore.GetAllPlannedMaintenance(ctx, g.orgID.StringValue())
|
||||
maintenance, err := g.maintenanceStore.ListPlannedMaintenance(ctx, g.orgID.StringValue())
|
||||
|
||||
if err != nil {
|
||||
g.logger.ErrorContext(ctx, "error in processing sql query", errors.Attr(err))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -40,12 +40,12 @@ func (m *MockSQLRuleStore) Mock() sqlmock.Sqlmock {
|
||||
}
|
||||
|
||||
// CreateRule implements ruletypes.RuleStore - delegates to underlying ruleStore to trigger SQL.
|
||||
func (m *MockSQLRuleStore) CreateRule(ctx context.Context, rule *ruletypes.Rule, fn func(context.Context, valuer.UUID) error) (valuer.UUID, error) {
|
||||
func (m *MockSQLRuleStore) CreateRule(ctx context.Context, rule *ruletypes.StorableRule, fn func(context.Context, valuer.UUID) error) (valuer.UUID, error) {
|
||||
return m.ruleStore.CreateRule(ctx, rule, fn)
|
||||
}
|
||||
|
||||
// EditRule implements ruletypes.RuleStore - delegates to underlying ruleStore to trigger SQL.
|
||||
func (m *MockSQLRuleStore) EditRule(ctx context.Context, rule *ruletypes.Rule, fn func(context.Context) error) error {
|
||||
func (m *MockSQLRuleStore) EditRule(ctx context.Context, rule *ruletypes.StorableRule, fn func(context.Context) error) error {
|
||||
return m.ruleStore.EditRule(ctx, rule, fn)
|
||||
}
|
||||
|
||||
@@ -55,12 +55,12 @@ func (m *MockSQLRuleStore) DeleteRule(ctx context.Context, id valuer.UUID, fn fu
|
||||
}
|
||||
|
||||
// GetStoredRule implements ruletypes.RuleStore - delegates to underlying ruleStore to trigger SQL.
|
||||
func (m *MockSQLRuleStore) GetStoredRule(ctx context.Context, id valuer.UUID) (*ruletypes.Rule, error) {
|
||||
func (m *MockSQLRuleStore) GetStoredRule(ctx context.Context, id valuer.UUID) (*ruletypes.StorableRule, error) {
|
||||
return m.ruleStore.GetStoredRule(ctx, id)
|
||||
}
|
||||
|
||||
// GetStoredRules implements ruletypes.RuleStore - delegates to underlying ruleStore to trigger SQL.
|
||||
func (m *MockSQLRuleStore) GetStoredRules(ctx context.Context, orgID string) ([]*ruletypes.Rule, error) {
|
||||
func (m *MockSQLRuleStore) GetStoredRules(ctx context.Context, orgID string) ([]*ruletypes.StorableRule, error) {
|
||||
return m.ruleStore.GetStoredRules(ctx, orgID)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func (m *MockSQLRuleStore) GetStoredRulesByMetricName(ctx context.Context, orgID
|
||||
}
|
||||
|
||||
// ExpectCreateRule sets up SQL expectations for CreateRule operation.
|
||||
func (m *MockSQLRuleStore) ExpectCreateRule(rule *ruletypes.Rule) {
|
||||
func (m *MockSQLRuleStore) ExpectCreateRule(rule *ruletypes.StorableRule) {
|
||||
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "created_by", "updated_by", "deleted", "data", "org_id"}).
|
||||
AddRow(rule.ID, rule.CreatedAt, rule.UpdatedAt, rule.CreatedBy, rule.UpdatedBy, rule.Deleted, rule.Data, rule.OrgID)
|
||||
expectedPattern := `INSERT INTO "rule" \(.+\) VALUES \(.+` +
|
||||
@@ -81,7 +81,7 @@ func (m *MockSQLRuleStore) ExpectCreateRule(rule *ruletypes.Rule) {
|
||||
}
|
||||
|
||||
// ExpectEditRule sets up SQL expectations for EditRule operation.
|
||||
func (m *MockSQLRuleStore) ExpectEditRule(rule *ruletypes.Rule) {
|
||||
func (m *MockSQLRuleStore) ExpectEditRule(rule *ruletypes.StorableRule) {
|
||||
expectedPattern := `UPDATE "rule".+` + rule.UpdatedBy + `.+` + rule.OrgID + `.+WHERE \(id = '` + rule.ID.StringValue() + `'\)`
|
||||
m.mock.ExpectExec(expectedPattern).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
@@ -95,7 +95,7 @@ func (m *MockSQLRuleStore) ExpectDeleteRule(ruleID valuer.UUID) {
|
||||
}
|
||||
|
||||
// ExpectGetStoredRule sets up SQL expectations for GetStoredRule operation.
|
||||
func (m *MockSQLRuleStore) ExpectGetStoredRule(ruleID valuer.UUID, rule *ruletypes.Rule) {
|
||||
func (m *MockSQLRuleStore) ExpectGetStoredRule(ruleID valuer.UUID, rule *ruletypes.StorableRule) {
|
||||
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "created_by", "updated_by", "deleted", "data", "org_id"}).
|
||||
AddRow(rule.ID, rule.CreatedAt, rule.UpdatedAt, rule.CreatedBy, rule.UpdatedBy, rule.Deleted, rule.Data, rule.OrgID)
|
||||
expectedPattern := `SELECT (.+) FROM "rule".+WHERE \(id = '` + ruleID.StringValue() + `'\)`
|
||||
@@ -104,7 +104,7 @@ func (m *MockSQLRuleStore) ExpectGetStoredRule(ruleID valuer.UUID, rule *ruletyp
|
||||
}
|
||||
|
||||
// ExpectGetStoredRules sets up SQL expectations for GetStoredRules operation.
|
||||
func (m *MockSQLRuleStore) ExpectGetStoredRules(orgID string, rules []*ruletypes.Rule) {
|
||||
func (m *MockSQLRuleStore) ExpectGetStoredRules(orgID string, rules []*ruletypes.StorableRule) {
|
||||
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "created_by", "updated_by", "deleted", "data", "org_id"})
|
||||
for _, rule := range rules {
|
||||
rows.AddRow(rule.ID, rule.CreatedAt, rule.UpdatedAt, rule.CreatedBy, rule.UpdatedBy, rule.Deleted, rule.Data, rule.OrgID)
|
||||
@@ -115,7 +115,7 @@ func (m *MockSQLRuleStore) ExpectGetStoredRules(orgID string, rules []*ruletypes
|
||||
}
|
||||
|
||||
// ExpectGetStoredRulesByMetricName sets up SQL expectations for GetStoredRulesByMetricName operation.
|
||||
func (m *MockSQLRuleStore) ExpectGetStoredRulesByMetricName(orgID string, metricName string, rules []*ruletypes.Rule) {
|
||||
func (m *MockSQLRuleStore) ExpectGetStoredRulesByMetricName(orgID string, metricName string, rules []*ruletypes.StorableRule) {
|
||||
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "created_by", "updated_by", "deleted", "data", "org_id"})
|
||||
for _, rule := range rules {
|
||||
rows.AddRow(rule.ID, rule.CreatedAt, rule.UpdatedAt, rule.CreatedBy, rule.UpdatedBy, rule.Deleted, rule.Data, rule.OrgID)
|
||||
|
||||
@@ -20,8 +20,8 @@ func NewMaintenanceStore(store sqlstore.SQLStore) ruletypes.MaintenanceStore {
|
||||
return &maintenance{sqlstore: store}
|
||||
}
|
||||
|
||||
func (r *maintenance) GetAllPlannedMaintenance(ctx context.Context, orgID string) ([]*ruletypes.GettablePlannedMaintenance, error) {
|
||||
gettableMaintenancesRules := make([]*ruletypes.GettablePlannedMaintenanceRule, 0)
|
||||
func (r *maintenance) ListPlannedMaintenance(ctx context.Context, orgID string) ([]*ruletypes.PlannedMaintenance, error) {
|
||||
gettableMaintenancesRules := make([]*ruletypes.PlannedMaintenanceWithRules, 0)
|
||||
err := r.sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
@@ -33,16 +33,16 @@ func (r *maintenance) GetAllPlannedMaintenance(ctx context.Context, orgID string
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gettablePlannedMaintenance := make([]*ruletypes.GettablePlannedMaintenance, 0)
|
||||
gettablePlannedMaintenance := make([]*ruletypes.PlannedMaintenance, 0)
|
||||
for _, gettableMaintenancesRule := range gettableMaintenancesRules {
|
||||
gettablePlannedMaintenance = append(gettablePlannedMaintenance, gettableMaintenancesRule.ConvertGettableMaintenanceRuleToGettableMaintenance())
|
||||
gettablePlannedMaintenance = append(gettablePlannedMaintenance, gettableMaintenancesRule.ToPlannedMaintenance())
|
||||
}
|
||||
|
||||
return gettablePlannedMaintenance, nil
|
||||
}
|
||||
|
||||
func (r *maintenance) GetPlannedMaintenanceByID(ctx context.Context, id valuer.UUID) (*ruletypes.GettablePlannedMaintenance, error) {
|
||||
storableMaintenanceRule := new(ruletypes.GettablePlannedMaintenanceRule)
|
||||
func (r *maintenance) GetPlannedMaintenanceByID(ctx context.Context, id valuer.UUID) (*ruletypes.PlannedMaintenance, error) {
|
||||
storableMaintenanceRule := new(ruletypes.PlannedMaintenanceWithRules)
|
||||
err := r.sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
@@ -51,16 +51,16 @@ 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
|
||||
return storableMaintenanceRule.ToPlannedMaintenance(), nil
|
||||
}
|
||||
|
||||
func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance ruletypes.GettablePlannedMaintenance) (valuer.UUID, error) {
|
||||
func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance *ruletypes.PostablePlannedMaintenance) (*ruletypes.PlannedMaintenance, error) {
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
return valuer.UUID{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
storablePlannedMaintenance := ruletypes.StorablePlannedMaintenance{
|
||||
@@ -82,10 +82,10 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
|
||||
}
|
||||
|
||||
maintenanceRules := make([]*ruletypes.StorablePlannedMaintenanceRule, 0)
|
||||
for _, ruleIDStr := range maintenance.RuleIDs {
|
||||
for _, ruleIDStr := range maintenance.AlertIds {
|
||||
ruleID, err := valuer.NewUUID(ruleIDStr)
|
||||
if err != nil {
|
||||
return valuer.UUID{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maintenanceRules = append(maintenanceRules, &ruletypes.StorablePlannedMaintenanceRule{
|
||||
@@ -122,10 +122,20 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return valuer.UUID{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return storablePlannedMaintenance.ID, nil
|
||||
return &ruletypes.PlannedMaintenance{
|
||||
ID: storablePlannedMaintenance.ID,
|
||||
Name: storablePlannedMaintenance.Name,
|
||||
Description: storablePlannedMaintenance.Description,
|
||||
Schedule: storablePlannedMaintenance.Schedule,
|
||||
RuleIDs: maintenance.AlertIds,
|
||||
CreatedAt: storablePlannedMaintenance.CreatedAt,
|
||||
CreatedBy: storablePlannedMaintenance.CreatedBy,
|
||||
UpdatedAt: storablePlannedMaintenance.UpdatedAt,
|
||||
UpdatedBy: storablePlannedMaintenance.UpdatedBy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *maintenance) DeletePlannedMaintenance(ctx context.Context, id valuer.UUID) error {
|
||||
@@ -142,22 +152,27 @@ func (r *maintenance) DeletePlannedMaintenance(ctx context.Context, id valuer.UU
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *maintenance) EditPlannedMaintenance(ctx context.Context, maintenance ruletypes.GettablePlannedMaintenance, id valuer.UUID) error {
|
||||
func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance *ruletypes.PostablePlannedMaintenance, id valuer.UUID) error {
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existing, err := r.GetPlannedMaintenanceByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storablePlannedMaintenance := ruletypes.StorablePlannedMaintenance{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: id,
|
||||
},
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: maintenance.CreatedAt,
|
||||
CreatedAt: existing.CreatedAt,
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: maintenance.CreatedBy,
|
||||
CreatedBy: existing.CreatedBy,
|
||||
UpdatedBy: claims.Email,
|
||||
},
|
||||
Name: maintenance.Name,
|
||||
@@ -167,7 +182,7 @@ func (r *maintenance) EditPlannedMaintenance(ctx context.Context, maintenance ru
|
||||
}
|
||||
|
||||
storablePlannedMaintenanceRules := make([]*ruletypes.StorablePlannedMaintenanceRule, 0)
|
||||
for _, ruleIDStr := range maintenance.RuleIDs {
|
||||
for _, ruleIDStr := range maintenance.AlertIds {
|
||||
ruleID, err := valuer.NewUUID(ruleIDStr)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -30,7 +30,7 @@ func NewRuleStore(store sqlstore.SQLStore, queryParser queryparser.QueryParser,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rule) CreateRule(ctx context.Context, storedRule *ruletypes.Rule, cb func(context.Context, valuer.UUID) error) (valuer.UUID, error) {
|
||||
func (r *rule) CreateRule(ctx context.Context, storedRule *ruletypes.StorableRule, cb func(context.Context, valuer.UUID) error) (valuer.UUID, error) {
|
||||
err := r.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
|
||||
_, err := r.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -51,7 +51,7 @@ func (r *rule) CreateRule(ctx context.Context, storedRule *ruletypes.Rule, cb fu
|
||||
return storedRule.ID, nil
|
||||
}
|
||||
|
||||
func (r *rule) EditRule(ctx context.Context, storedRule *ruletypes.Rule, cb func(context.Context) error) error {
|
||||
func (r *rule) EditRule(ctx context.Context, storedRule *ruletypes.StorableRule, cb func(context.Context) error) error {
|
||||
return r.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
|
||||
_, err := r.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -72,7 +72,7 @@ func (r *rule) DeleteRule(ctx context.Context, id valuer.UUID, cb func(context.C
|
||||
_, err := r.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model(new(ruletypes.Rule)).
|
||||
Model(new(ruletypes.StorableRule)).
|
||||
Where("id = ?", id.StringValue()).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
@@ -87,8 +87,8 @@ func (r *rule) DeleteRule(ctx context.Context, id valuer.UUID, cb func(context.C
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rule) GetStoredRules(ctx context.Context, orgID string) ([]*ruletypes.Rule, error) {
|
||||
rules := make([]*ruletypes.Rule, 0)
|
||||
func (r *rule) GetStoredRules(ctx context.Context, orgID string) ([]*ruletypes.StorableRule, error) {
|
||||
rules := make([]*ruletypes.StorableRule, 0)
|
||||
err := r.sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
@@ -102,8 +102,8 @@ func (r *rule) GetStoredRules(ctx context.Context, orgID string) ([]*ruletypes.R
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (r *rule) GetStoredRule(ctx context.Context, id valuer.UUID) (*ruletypes.Rule, error) {
|
||||
rule := new(ruletypes.Rule)
|
||||
func (r *rule) GetStoredRule(ctx context.Context, id valuer.UUID) (*ruletypes.StorableRule, error) {
|
||||
rule := new(ruletypes.StorableRule)
|
||||
err := r.sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
@@ -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
|
||||
}
|
||||
|
||||
323
pkg/ruler/signozruler/handler.go
Normal file
323
pkg/ruler/signozruler/handler.go
Normal file
@@ -0,0 +1,323 @@
|
||||
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
|
||||
}
|
||||
|
||||
view := make([]*ruletypes.Rule, 0, len(rules.Rules))
|
||||
for _, rule := range rules.Rules {
|
||||
view = append(view, ruletypes.NewRule(rule))
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, view)
|
||||
}
|
||||
|
||||
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, ruletypes.NewRule(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, ruletypes.NewRule(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()
|
||||
|
||||
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.DeleteRule(ctx, id.StringValue())
|
||||
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, ruletypes.NewRule(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().ListPlannedMaintenance(ctx, claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if params.Active != nil {
|
||||
activeSchedules := make([]*ruletypes.PlannedMaintenance, 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.PlannedMaintenance, 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()
|
||||
|
||||
schedule := new(ruletypes.PostablePlannedMaintenance)
|
||||
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
|
||||
}
|
||||
|
||||
created, err := handler.ruler.MaintenanceStore().CreatePlannedMaintenance(ctx, schedule)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, created)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
schedule := new(ruletypes.PostablePlannedMaintenance)
|
||||
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().UpdatePlannedMaintenance(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"
|
||||
@@ -88,9 +85,9 @@ func NewCacheProviderFactories() factory.NamedMap[factory.ProviderFactory[cache.
|
||||
)
|
||||
}
|
||||
|
||||
func NewWebProviderFactories() factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]] {
|
||||
func NewWebProviderFactories(globalConfig global.Config) factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
routerweb.NewFactory(),
|
||||
routerweb.NewFactory(globalConfig),
|
||||
noopweb.NewFactory(),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfmanagertest"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"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"
|
||||
@@ -34,7 +34,7 @@ func TestNewProviderFactories(t *testing.T) {
|
||||
})
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
NewWebProviderFactories()
|
||||
NewWebProviderFactories(global.Config{})
|
||||
})
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
@@ -64,11 +64,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,
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package markdownrenderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
func (r *renderer) renderHTML(_ context.Context, markdown string) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := newHTMLRenderer().Convert([]byte(markdown), &buf); err != nil {
|
||||
return "", errors.WrapInternalf(err, errors.CodeInternal, "failed to convert markdown to HTML")
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
package markdownrenderer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
testMarkdown = `# 🔥 FIRING: High CPU Usage on api-gateway
|
||||
|
||||
https://signoz.example.com/alerts/123
|
||||
https://runbooks.example.com/cpu-high
|
||||
|
||||
## Alert Details
|
||||
|
||||
**Status:** **FIRING** | *api-gateway* service is experiencing high CPU usage. ~~resolved~~ previously.
|
||||
|
||||
Alert triggered because ` + "`cpu_usage_percent`" + ` exceeded threshold ` + "`90`" + `.
|
||||
|
||||
[View Alert in SigNoz](https://signoz.example.com/alerts/123) | [View Logs](https://signoz.example.com/logs?service=api-gateway) | [View Traces](https://signoz.example.com/traces?service=api-gateway)
|
||||
|
||||

|
||||
|
||||
## Alert Labels
|
||||
|
||||
| Label | Value |
|
||||
| -------- | ----------- |
|
||||
| service | api-gateway |
|
||||
| instance | pod-5a8b3c |
|
||||
| severity | critical |
|
||||
| region | us-east-1 |
|
||||
|
||||
## Remediation Steps
|
||||
|
||||
1. Check current CPU usage on the pod
|
||||
2. Review recent deployments for regressions
|
||||
3. Scale horizontally if load-related
|
||||
1. Increase replica count
|
||||
2. Verify HPA configuration
|
||||
|
||||
## Affected Services
|
||||
|
||||
* api-gateway
|
||||
* auth-service
|
||||
* payment-service
|
||||
* payment-processor
|
||||
* payment-validator
|
||||
|
||||
## Incident Checklist
|
||||
|
||||
- [x] Alert acknowledged
|
||||
- [x] On-call notified
|
||||
- [ ] Root cause identified
|
||||
- [ ] Fix deployed
|
||||
|
||||
## Alert Rule Description
|
||||
|
||||
> This alert fires when CPU usage exceeds 90% for more than 5 minutes on any pod in the api-gateway service.
|
||||
>
|
||||
>> For capacity planning guidelines, see the infrastructure runbook section on horizontal pod autoscaling.
|
||||
|
||||
## Triggered Query
|
||||
` + "```promql\navg(rate(container_cpu_usage_seconds_total{service=\"api-gateway\"}[5m])) by (pod) > 0.9\n```" + `
|
||||
|
||||
## Inline Details
|
||||
|
||||
This alert was generated by SigNoz using ` + "`alertmanager`" + ` rules engine.
|
||||
`
|
||||
)
|
||||
|
||||
func newTestRenderer() Renderer {
|
||||
return NewRenderer()
|
||||
}
|
||||
|
||||
func TestRenderHTML_Composite(t *testing.T) {
|
||||
renderer := newTestRenderer()
|
||||
|
||||
html, err := renderer.Render(context.Background(), testMarkdown, MarkdownFormatHTML)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Full expected output for exact match
|
||||
expected := "<h1>🔥 FIRING: High CPU Usage on api-gateway</h1>\n" +
|
||||
"<p><a href=\"https://signoz.example.com/alerts/123\">https://signoz.example.com/alerts/123</a>\n<a href=\"https://runbooks.example.com/cpu-high\">https://runbooks.example.com/cpu-high</a></p>\n" +
|
||||
"<h2>Alert Details</h2>\n" +
|
||||
"<p><strong>Status:</strong> <strong>FIRING</strong> | <em>api-gateway</em> service is experiencing high CPU usage. <del>resolved</del> previously.</p>\n" +
|
||||
"<p>Alert triggered because <code>cpu_usage_percent</code> exceeded threshold <code>90</code>.</p>\n" +
|
||||
"<p><a href=\"https://signoz.example.com/alerts/123\">View Alert in SigNoz</a> | <a href=\"https://signoz.example.com/logs?service=api-gateway\">View Logs</a> | <a href=\"https://signoz.example.com/traces?service=api-gateway\">View Traces</a></p>\n" +
|
||||
"<p><img src=\"https://signoz.example.com/badges/critical.svg\" alt=\"critical\" title=\"Critical Alert\"></p>\n" +
|
||||
"<h2>Alert Labels</h2>\n" +
|
||||
"<table>\n<thead>\n<tr>\n<th>Label</th>\n<th>Value</th>\n</tr>\n</thead>\n" +
|
||||
"<tbody>\n<tr>\n<td>service</td>\n<td>api-gateway</td>\n</tr>\n" +
|
||||
"<tr>\n<td>instance</td>\n<td>pod-5a8b3c</td>\n</tr>\n" +
|
||||
"<tr>\n<td>severity</td>\n<td>critical</td>\n</tr>\n" +
|
||||
"<tr>\n<td>region</td>\n<td>us-east-1</td>\n</tr>\n</tbody>\n</table>\n" +
|
||||
"<h2>Remediation Steps</h2>\n" +
|
||||
"<ol>\n<li>Check current CPU usage on the pod</li>\n<li>Review recent deployments for regressions</li>\n<li>Scale horizontally if load-related\n" +
|
||||
"<ol>\n<li>Increase replica count</li>\n<li>Verify HPA configuration</li>\n</ol>\n</li>\n</ol>\n" +
|
||||
"<h2>Affected Services</h2>\n" +
|
||||
"<ul>\n<li>api-gateway</li>\n<li>auth-service</li>\n<li>payment-service\n" +
|
||||
"<ul>\n<li>payment-processor</li>\n<li>payment-validator</li>\n</ul>\n</li>\n</ul>\n" +
|
||||
"<h2>Incident Checklist</h2>\n" +
|
||||
"<ul>\n<li><input checked=\"\" disabled=\"\" type=\"checkbox\"> Alert acknowledged</li>\n" +
|
||||
"<li><input checked=\"\" disabled=\"\" type=\"checkbox\"> On-call notified</li>\n" +
|
||||
"<li><input disabled=\"\" type=\"checkbox\"> Root cause identified</li>\n" +
|
||||
"<li><input disabled=\"\" type=\"checkbox\"> Fix deployed</li>\n</ul>\n" +
|
||||
"<h2>Alert Rule Description</h2>\n" +
|
||||
"<blockquote>\n<p>This alert fires when CPU usage exceeds 90% for more than 5 minutes on any pod in the api-gateway service.</p>\n" +
|
||||
"<blockquote>\n<p>For capacity planning guidelines, see the infrastructure runbook section on horizontal pod autoscaling.</p>\n</blockquote>\n</blockquote>\n" +
|
||||
"<h2>Triggered Query</h2>\n" +
|
||||
"<pre><code class=\"language-promql\">avg(rate(container_cpu_usage_seconds_total{service="api-gateway"}[5m])) by (pod) > 0.9\n</code></pre>\n" +
|
||||
"<h2>Inline Details</h2>\n" +
|
||||
"<p>This alert was generated by SigNoz using <code>alertmanager</code> rules engine.</p>\n"
|
||||
|
||||
assert.Equal(t, expected, html)
|
||||
}
|
||||
|
||||
func TestRenderHTML_InlineFormatting(t *testing.T) {
|
||||
renderer := newTestRenderer()
|
||||
|
||||
input := `# 🔥 FIRING: High CPU on api-gateway
|
||||
## Alert Status
|
||||
|
||||
**FIRING** alert for *api-gateway* service — ~~resolved~~ previously.
|
||||
|
||||
Metric ` + "`cpu_usage_percent`" + ` exceeded threshold. [View in SigNoz](https://signoz.example.com/alerts/123)
|
||||
|
||||
`
|
||||
|
||||
html, err := renderer.Render(context.Background(), input, MarkdownFormatHTML)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := "<h1>🔥 FIRING: High CPU on api-gateway</h1>\n<h2>Alert Status</h2>\n" +
|
||||
"<p><strong>FIRING</strong> alert for <em>api-gateway</em> service — <del>resolved</del> previously.</p>\n" +
|
||||
"<p>Metric <code>cpu_usage_percent</code> exceeded threshold. <a href=\"https://signoz.example.com/alerts/123\">View in SigNoz</a></p>\n" +
|
||||
"<p><img src=\"https://signoz.example.com/badges/critical.svg\" alt=\"critical\" title=\"Critical Alert\"></p>\n"
|
||||
|
||||
assert.Equal(t, expected, html)
|
||||
}
|
||||
|
||||
func TestRenderHTML_BlockElements(t *testing.T) {
|
||||
renderer := newTestRenderer()
|
||||
|
||||
input := `1. Check CPU usage on the pod
|
||||
2. Review recent deployments
|
||||
3. Scale horizontally if needed
|
||||
|
||||
* api-gateway
|
||||
* auth-service
|
||||
* payment-service
|
||||
|
||||
- [x] Alert acknowledged
|
||||
- [ ] Root cause identified
|
||||
|
||||
> This alert fires when CPU usage exceeds 90% for more than 5 minutes.
|
||||
|
||||
| Label | Value |
|
||||
| -------- | ----------- |
|
||||
| service | api-gateway |
|
||||
| severity | <no value> |
|
||||
|
||||
` + "```promql\navg(rate(container_cpu_usage_seconds_total{service=\"api-gateway\"}[5m])) by (pod) > 0.9\n```"
|
||||
|
||||
html, err := renderer.Render(context.Background(), input, MarkdownFormatHTML)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := "<ol>\n<li>Check CPU usage on the pod</li>\n<li>Review recent deployments</li>\n<li>Scale horizontally if needed</li>\n</ol>\n" +
|
||||
"<ul>\n<li>api-gateway</li>\n<li>auth-service</li>\n<li>payment-service</li>\n</ul>\n" +
|
||||
"<ul>\n<li><input checked=\"\" disabled=\"\" type=\"checkbox\"> Alert acknowledged</li>\n" +
|
||||
"<li><input disabled=\"\" type=\"checkbox\"> Root cause identified</li>\n</ul>\n" +
|
||||
"<blockquote>\n<p>This alert fires when CPU usage exceeds 90% for more than 5 minutes.</p>\n</blockquote>\n" +
|
||||
"<table>\n<thead>\n<tr>\n<th>Label</th>\n<th>Value</th>\n</tr>\n</thead>\n" +
|
||||
"<tbody>\n<tr>\n<td>service</td>\n<td>api-gateway</td>\n</tr>\n" +
|
||||
"<tr>\n<td>severity</td>\n<td><no value></td>\n</tr>\n</tbody>\n</table>\n" +
|
||||
"<pre><code class=\"language-promql\">avg(rate(container_cpu_usage_seconds_total{service="api-gateway"}[5m])) by (pod) > 0.9\n</code></pre>\n"
|
||||
|
||||
assert.Equal(t, expected, html)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package markdownrenderer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/templating/slackblockkitrenderer"
|
||||
"github.com/SigNoz/signoz/pkg/templating/slackmrkdwnrenderer"
|
||||
"github.com/SigNoz/signoz/pkg/templating/templatingextensions"
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
)
|
||||
|
||||
// newHTMLRenderer creates a new goldmark.Markdown instance for HTML rendering.
|
||||
func newHTMLRenderer() goldmark.Markdown {
|
||||
return goldmark.New(
|
||||
goldmark.WithExtensions(extension.GFM),
|
||||
goldmark.WithExtensions(templatingextensions.EscapeNoValue),
|
||||
)
|
||||
}
|
||||
|
||||
// newSlackBlockKitRenderer creates a new goldmark.Markdown instance for Slack Block Kit rendering.
|
||||
func newSlackBlockKitRenderer() goldmark.Markdown {
|
||||
return goldmark.New(
|
||||
goldmark.WithExtensions(slackblockkitrenderer.BlockKitV2),
|
||||
)
|
||||
}
|
||||
|
||||
// newSlackMrkdwnRenderer creates a new goldmark.Markdown instance for Slack mrkdwn rendering.
|
||||
func newSlackMrkdwnRenderer() goldmark.Markdown {
|
||||
return goldmark.New(
|
||||
goldmark.WithExtensions(slackmrkdwnrenderer.SlackMrkdwn),
|
||||
)
|
||||
}
|
||||
|
||||
type OutputFormat int
|
||||
|
||||
const (
|
||||
MarkdownFormatHTML OutputFormat = iota
|
||||
MarkdownFormatSlackBlockKit
|
||||
MarkdownFormatSlackMrkdwn
|
||||
MarkdownFormatNoop
|
||||
)
|
||||
|
||||
// Renderer is the interface for rendering markdown to different formats.
|
||||
type Renderer interface {
|
||||
// Render renders the markdown to the given output format.
|
||||
Render(ctx context.Context, markdown string, outputFormat OutputFormat) (string, error)
|
||||
}
|
||||
|
||||
type renderer struct {
|
||||
}
|
||||
|
||||
func NewRenderer() Renderer {
|
||||
return &renderer{}
|
||||
}
|
||||
|
||||
func (r *renderer) Render(ctx context.Context, markdown string, outputFormat OutputFormat) (string, error) {
|
||||
switch outputFormat {
|
||||
case MarkdownFormatHTML:
|
||||
return r.renderHTML(ctx, markdown)
|
||||
case MarkdownFormatSlackBlockKit:
|
||||
return r.renderSlackBlockKit(ctx, markdown)
|
||||
case MarkdownFormatSlackMrkdwn:
|
||||
return r.renderSlackMrkdwn(ctx, markdown)
|
||||
case MarkdownFormatNoop:
|
||||
return markdown, nil
|
||||
default:
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unknown output format: %v", outputFormat)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package markdownrenderer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRenderNoop(t *testing.T) {
|
||||
renderer := newTestRenderer()
|
||||
|
||||
output, err := renderer.Render(context.Background(), testMarkdown, MarkdownFormatNoop)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testMarkdown, output)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package markdownrenderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
func (r *renderer) renderSlackBlockKit(_ context.Context, markdown string) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := newSlackBlockKitRenderer().Convert([]byte(markdown), &buf); err != nil {
|
||||
return "", errors.WrapInternalf(err, errors.CodeInternal, "failed to convert markdown to Slack Block Kit")
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (r *renderer) renderSlackMrkdwn(_ context.Context, markdown string) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := newSlackMrkdwnRenderer().Convert([]byte(markdown), &buf); err != nil {
|
||||
return "", errors.WrapInternalf(err, errors.CodeInternal, "failed to convert markdown to Slack Mrkdwn")
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
package markdownrenderer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func jsonEqual(a, b string) bool {
|
||||
var va, vb any
|
||||
if err := json.Unmarshal([]byte(a), &va); err != nil {
|
||||
return false
|
||||
}
|
||||
if err := json.Unmarshal([]byte(b), &vb); err != nil {
|
||||
return false
|
||||
}
|
||||
ja, _ := json.Marshal(va)
|
||||
jb, _ := json.Marshal(vb)
|
||||
return string(ja) == string(jb)
|
||||
}
|
||||
|
||||
func prettyJSON(s string) string {
|
||||
var v any
|
||||
if err := json.Unmarshal([]byte(s), &v); err != nil {
|
||||
return s
|
||||
}
|
||||
b, _ := json.MarshalIndent(v, "", " ")
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestRenderSlackBlockKit(t *testing.T) {
|
||||
renderer := NewRenderer()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
markdown string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple paragraph",
|
||||
markdown: "Hello world",
|
||||
expected: `[
|
||||
{
|
||||
"type": "section",
|
||||
"text": { "type": "mrkdwn", "text": "Hello world" }
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "alert-themed with heading, list, and code block",
|
||||
markdown: `# Alert Triggered
|
||||
|
||||
- Service: **checkout-api**
|
||||
- Status: _critical_
|
||||
|
||||
` + "```" + `
|
||||
error: connection timeout after 30s
|
||||
` + "```",
|
||||
expected: `[
|
||||
{
|
||||
"type": "section",
|
||||
"text": { "type": "mrkdwn", "text": "*Alert Triggered*" }
|
||||
},
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_list", "style": "bullet", "indent": 0, "border": 0,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [
|
||||
{ "type": "text", "text": "Service: " },
|
||||
{ "type": "text", "text": "checkout-api", "style": { "bold": true } }
|
||||
]},
|
||||
{ "type": "rich_text_section", "elements": [
|
||||
{ "type": "text", "text": "Status: " },
|
||||
{ "type": "text", "text": "critical", "style": { "italic": true } }
|
||||
]}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_preformatted",
|
||||
"border": 0,
|
||||
"elements": [
|
||||
{ "type": "text", "text": "error: connection timeout after 30s" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := renderer.Render(context.Background(), tt.markdown, MarkdownFormatSlackBlockKit)
|
||||
if err != nil {
|
||||
t.Fatalf("Render error: %v", err)
|
||||
}
|
||||
|
||||
// Verify output is valid JSON
|
||||
if !json.Valid([]byte(got)) {
|
||||
t.Fatalf("output is not valid JSON:\n%s", got)
|
||||
}
|
||||
|
||||
if !jsonEqual(got, tt.expected) {
|
||||
t.Errorf("JSON mismatch\n\nMarkdown:\n%s\n\nExpected:\n%s\n\nGot:\n%s",
|
||||
tt.markdown, prettyJSON(tt.expected), prettyJSON(got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderSlackMrkdwn(t *testing.T) {
|
||||
renderer := NewRenderer()
|
||||
|
||||
markdown := `# Alert Triggered
|
||||
|
||||
- Service: **checkout-api**
|
||||
- Status: _critical_
|
||||
- Dashboard: [View Dashboard](https://example.com/dashboard)
|
||||
|
||||
| Metric | Value | Threshold |
|
||||
| --- | --- | --- |
|
||||
| Latency | 250ms | 100ms |
|
||||
| Error Rate | 5.2% | 1% |
|
||||
|
||||
` + "```" + `
|
||||
error: connection timeout after 30s
|
||||
` + "```"
|
||||
|
||||
expected := "*Alert Triggered*\n\n" +
|
||||
"• Service: *checkout-api*\n" +
|
||||
"• Status: _critical_\n" +
|
||||
"• Dashboard: <https://example.com/dashboard|View Dashboard>\n\n" +
|
||||
"```\nMetric | Value | Threshold\n-----------|-------|----------\nLatency | 250ms | 100ms \nError Rate | 5.2% | 1% \n```\n\n" +
|
||||
"```\nerror: connection timeout after 30s\n```\n\n"
|
||||
|
||||
got, err := renderer.Render(context.Background(), markdown, MarkdownFormatSlackMrkdwn)
|
||||
if err != nil {
|
||||
t.Fatalf("Render error: %v", err)
|
||||
}
|
||||
|
||||
if got != expected {
|
||||
t.Errorf("mrkdwn mismatch\n\nExpected:\n%q\n\nGot:\n%q", expected, got)
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package slackblockkitrenderer
|
||||
|
||||
import (
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
type blockKitV2 struct{}
|
||||
|
||||
// BlockKitV2 is a goldmark.Extender that configures the Slack Block Kit v2 renderer.
|
||||
var BlockKitV2 = &blockKitV2{}
|
||||
|
||||
// Extend implements goldmark.Extender.
|
||||
func (e *blockKitV2) Extend(m goldmark.Markdown) {
|
||||
extension.Table.Extend(m)
|
||||
extension.Strikethrough.Extend(m)
|
||||
extension.TaskList.Extend(m)
|
||||
m.Renderer().AddOptions(
|
||||
renderer.WithNodeRenderers(util.Prioritized(NewRenderer(), 1)),
|
||||
)
|
||||
}
|
||||
@@ -1,542 +0,0 @@
|
||||
package slackblockkitrenderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
)
|
||||
|
||||
func jsonEqual(a, b string) bool {
|
||||
var va, vb interface{}
|
||||
if err := json.Unmarshal([]byte(a), &va); err != nil {
|
||||
return false
|
||||
}
|
||||
if err := json.Unmarshal([]byte(b), &vb); err != nil {
|
||||
return false
|
||||
}
|
||||
ja, _ := json.Marshal(va)
|
||||
jb, _ := json.Marshal(vb)
|
||||
return string(ja) == string(jb)
|
||||
}
|
||||
|
||||
func prettyJSON(s string) string {
|
||||
var v interface{}
|
||||
if err := json.Unmarshal([]byte(s), &v); err != nil {
|
||||
return s
|
||||
}
|
||||
b, _ := json.MarshalIndent(v, "", " ")
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestRenderer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
markdown string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
markdown: "",
|
||||
expected: `[]`,
|
||||
},
|
||||
{
|
||||
name: "simple paragraph",
|
||||
markdown: "Hello world",
|
||||
expected: `[
|
||||
{
|
||||
"type": "section",
|
||||
"text": { "type": "mrkdwn", "text": "Hello world" }
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "heading",
|
||||
markdown: "# My Heading",
|
||||
expected: `[
|
||||
{
|
||||
"type": "section",
|
||||
"text": { "type": "mrkdwn", "text": "*My Heading*" }
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "multiple paragraphs",
|
||||
markdown: "First paragraph\n\nSecond paragraph",
|
||||
expected: `[
|
||||
{
|
||||
"type": "section",
|
||||
"text": { "type": "mrkdwn", "text": "First paragraph\nSecond paragraph" }
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "todo list ",
|
||||
markdown: "- [ ] item 1\n- [x] item 2",
|
||||
expected: `[
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"border": 0,
|
||||
"elements": [
|
||||
{ "elements": [ { "text": "[ ] ", "type": "text" }, { "text": "item 1", "type": "text" } ], "type": "rich_text_section" },
|
||||
{ "elements": [ { "text": "[x] ", "type": "text" }, { "text": "item 2", "type": "text" } ], "type": "rich_text_section" }
|
||||
],
|
||||
"indent": 0,
|
||||
"style": "bullet",
|
||||
"type": "rich_text_list"
|
||||
}
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "thematic break between paragraphs",
|
||||
markdown: "Before\n\n---\n\nAfter",
|
||||
expected: `[
|
||||
{ "type": "section", "text": { "type": "mrkdwn", "text": "Before" } },
|
||||
{ "type": "divider" },
|
||||
{ "type": "section", "text": { "type": "mrkdwn", "text": "After" } }
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "fenced code block with language",
|
||||
markdown: "```go\nfmt.Println(\"hello\")\n```",
|
||||
expected: `[
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_preformatted",
|
||||
"border": 0,
|
||||
"language": "go",
|
||||
"elements": [
|
||||
{ "type": "text", "text": "fmt.Println(\"hello\")" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "indented code block",
|
||||
markdown: " code line 1\n code line 2",
|
||||
expected: `[
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_preformatted",
|
||||
"border": 0,
|
||||
"elements": [
|
||||
{ "type": "text", "text": "code line 1\ncode line 2" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "empty fenced code block",
|
||||
markdown: "```\n```",
|
||||
expected: `[
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_preformatted",
|
||||
"border": 0,
|
||||
"elements": [
|
||||
{ "type": "text", "text": " " }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "simple bullet list",
|
||||
markdown: "- item 1\n- item 2\n- item 3",
|
||||
expected: `[
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_list", "style": "bullet", "indent": 0, "border": 0,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "item 1" }] },
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "item 2" }] },
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "item 3" }] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "simple ordered list",
|
||||
markdown: "1. first\n2. second\n3. third",
|
||||
expected: `[
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_list", "style": "ordered", "indent": 0, "border": 0,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "first" }] },
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "second" }] },
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "third" }] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "nested bullet list (2 levels)",
|
||||
markdown: "- item 1\n- item 2\n - sub a\n - sub b\n- item 3",
|
||||
expected: `[
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_list", "style": "bullet", "indent": 0, "border": 0,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "item 1" }] },
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "item 2" }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "rich_text_list", "style": "bullet", "indent": 1, "border": 0,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "sub a" }] },
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "sub b" }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "rich_text_list", "style": "bullet", "indent": 0, "border": 0,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "item 3" }] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "nested ordered list with offset",
|
||||
markdown: "1. first\n 1. nested-a\n 2. nested-b\n2. second\n3. third",
|
||||
expected: `[
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_list", "style": "ordered", "indent": 0, "border": 0,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "first" }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "rich_text_list", "style": "ordered", "indent": 1, "border": 0,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "nested-a" }] },
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "nested-b" }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "rich_text_list", "style": "ordered", "indent": 0, "border": 0, "offset": 1,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "second" }] },
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "third" }] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "mixed ordered/bullet nesting",
|
||||
markdown: "1. ordered\n - bullet child\n2. ordered again",
|
||||
expected: `[
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_list", "style": "ordered", "indent": 0, "border": 0,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "ordered" }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "rich_text_list", "style": "bullet", "indent": 1, "border": 0,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "bullet child" }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "rich_text_list", "style": "ordered", "indent": 0, "border": 0, "offset": 1,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "ordered again" }] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "list items with bold/italic/link/code",
|
||||
markdown: "- **bold item**\n- _italic item_\n- [link](http://example.com)\n- `code item`",
|
||||
expected: `[
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_list", "style": "bullet", "indent": 0, "border": 0,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "bold item", "style": { "bold": true } }] },
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "italic item", "style": { "italic": true } }] },
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "link", "url": "http://example.com", "text": "link" }] },
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "code item", "style": { "code": true } }] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "table with header and body",
|
||||
markdown: "| Name | Age |\n|------|-----|\n| Alice | 30 |",
|
||||
expected: `[
|
||||
{
|
||||
"type": "table",
|
||||
"rows": [
|
||||
[
|
||||
{ "type": "rich_text", "elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "Name", "style": { "bold": true } }] }
|
||||
]},
|
||||
{ "type": "rich_text", "elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "Age", "style": { "bold": true } }] }
|
||||
]}
|
||||
],
|
||||
[
|
||||
{ "type": "rich_text", "elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "Alice" }] }
|
||||
]},
|
||||
{ "type": "rich_text", "elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "30" }] }
|
||||
]}
|
||||
]
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "blockquote",
|
||||
markdown: "> quoted text",
|
||||
expected: `[
|
||||
{
|
||||
"type": "section",
|
||||
"text": { "type": "mrkdwn", "text": "> quoted text" }
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "blockquote with nested list",
|
||||
markdown: "> item 1\n> > item 2\n> > item 3",
|
||||
expected: `[
|
||||
{
|
||||
"text": {
|
||||
"text": "> item 1\n> > item 2\n> > item 3",
|
||||
"type": "mrkdwn"
|
||||
},
|
||||
"type": "section"
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "inline formatting in paragraph",
|
||||
markdown: "This is **bold** and _italic_ and ~strike~ and `code`",
|
||||
expected: `[
|
||||
{
|
||||
"type": "section",
|
||||
"text": { "type": "mrkdwn", "text": "This is *bold* and _italic_ and ~strike~ and ` + "`code`" + `" }
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "link in paragraph",
|
||||
markdown: "Visit [Google](http://google.com)",
|
||||
expected: `[
|
||||
{
|
||||
"type": "section",
|
||||
"text": { "type": "mrkdwn", "text": "Visit <http://google.com|Google>" }
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "image is skipped",
|
||||
markdown: "",
|
||||
// For image skip the block and return empty array
|
||||
expected: `[]`,
|
||||
},
|
||||
{
|
||||
name: "paragraph then list then paragraph",
|
||||
markdown: "Before\n\n- item\n\nAfter",
|
||||
expected: `[
|
||||
{ "type": "section", "text": { "type": "mrkdwn", "text": "Before" } },
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_list", "style": "bullet", "indent": 0, "border": 0,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "item" }] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ "type": "section", "text": { "type": "mrkdwn", "text": "After" } }
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "ordered list with start > 1",
|
||||
markdown: "5. fifth\n6. sixth",
|
||||
expected: `[
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_list", "style": "ordered", "indent": 0, "border": 0, "offset": 4,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "fifth" }] },
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "sixth" }] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "deeply nested ordered list (3 levels) with offsets",
|
||||
markdown: "1. Some things\n\t1. are best left\n2. to the fate\n\t1. of the world\n\t\t1. and then\n\t\t2. this is how\n3. it turns out to be",
|
||||
expected: `[
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{ "type": "rich_text_list", "style": "ordered", "indent": 0, "border": 0,
|
||||
"elements": [{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "Some things" }] }] },
|
||||
|
||||
{ "type": "rich_text_list", "style": "ordered", "indent": 1, "border": 0,
|
||||
"elements": [{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "are best left" }] }] },
|
||||
|
||||
{ "type": "rich_text_list", "style": "ordered", "indent": 0, "border": 0, "offset": 1,
|
||||
"elements": [{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "to the fate" }] }] },
|
||||
|
||||
{ "type": "rich_text_list", "style": "ordered", "indent": 1, "border": 0,
|
||||
"elements": [{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "of the world" }] }] },
|
||||
|
||||
{ "type": "rich_text_list", "style": "ordered", "indent": 2, "border": 0,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "and then" }] },
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "this is how" }] }
|
||||
]
|
||||
},
|
||||
|
||||
{ "type": "rich_text_list", "style": "ordered", "indent": 0, "border": 0, "offset": 2,
|
||||
"elements": [{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "it turns out to be" }] }] }
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "link with bold label in list item",
|
||||
markdown: "- [**docs**](http://example.com)",
|
||||
expected: `[
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_list", "style": "bullet", "indent": 0, "border": 0,
|
||||
"elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "link", "url": "http://example.com", "text": "docs" }] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "table with empty cell",
|
||||
markdown: "| A | B |\n|---|---|\n| 1 | |",
|
||||
expected: `[
|
||||
{
|
||||
"type": "table",
|
||||
"rows": [
|
||||
[
|
||||
{ "type": "rich_text", "elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "A", "style": { "bold": true } }] }
|
||||
]},
|
||||
{ "type": "rich_text", "elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "B", "style": { "bold": true } }] }
|
||||
]}
|
||||
],
|
||||
[
|
||||
{ "type": "rich_text", "elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "1" }] }
|
||||
]},
|
||||
{ "type": "rich_text", "elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": " " }] }
|
||||
]}
|
||||
]
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "table with missing column in row",
|
||||
markdown: "| A | B |\n|---|---|\n| 1 |",
|
||||
expected: `[
|
||||
{
|
||||
"type": "table",
|
||||
"rows": [
|
||||
[
|
||||
{ "type": "rich_text", "elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "A", "style": { "bold": true } }] }
|
||||
]},
|
||||
{ "type": "rich_text", "elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "B", "style": { "bold": true } }] }
|
||||
]}
|
||||
],
|
||||
[
|
||||
{ "type": "rich_text", "elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": "1" }] }
|
||||
]},
|
||||
{ "type": "rich_text", "elements": [
|
||||
{ "type": "rich_text_section", "elements": [{ "type": "text", "text": " " }] }
|
||||
]}
|
||||
]
|
||||
]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
md := goldmark.New(
|
||||
goldmark.WithExtensions(BlockKitV2),
|
||||
)
|
||||
var buf bytes.Buffer
|
||||
if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
|
||||
t.Fatalf("convert error: %v", err)
|
||||
}
|
||||
got := buf.String()
|
||||
if !jsonEqual(got, tt.expected) {
|
||||
t.Errorf("JSON mismatch\n\nMarkdown:\n%s\n\nExpected:\n%s\n\nGot:\n%s",
|
||||
tt.markdown, prettyJSON(tt.expected), prettyJSON(got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,737 +0,0 @@
|
||||
package slackblockkitrenderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
extensionast "github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
// listFrame tracks state for a single level of list nesting.
|
||||
type listFrame struct {
|
||||
style string // "bullet" or "ordered"
|
||||
indent int
|
||||
itemCount int
|
||||
}
|
||||
|
||||
// listContext holds all state while processing a list tree.
|
||||
type listContext struct {
|
||||
result []RichTextList
|
||||
stack []listFrame
|
||||
current *RichTextList
|
||||
currentItemInlines []interface{}
|
||||
}
|
||||
|
||||
// tableContext holds state while processing a table.
|
||||
type tableContext struct {
|
||||
rows [][]TableCell
|
||||
currentRow []TableCell
|
||||
currentCellInlines []interface{}
|
||||
isHeader bool
|
||||
}
|
||||
|
||||
// Renderer converts Markdown AST to Slack Block Kit JSON.
|
||||
type Renderer struct {
|
||||
blocks []interface{}
|
||||
mrkdwn strings.Builder
|
||||
// holds active styles for the current rich text element
|
||||
styleStack []RichTextStyle
|
||||
// holds the current list context while processing a list tree.
|
||||
listCtx *listContext
|
||||
// holds the current table context while processing a table.
|
||||
tableCtx *tableContext
|
||||
// stores the current blockquote depth while processing a blockquote.
|
||||
// so blockquote with nested list can be rendered correctly.
|
||||
blockquoteDepth int
|
||||
}
|
||||
|
||||
// NewRenderer returns a new block kit renderer.
|
||||
func NewRenderer() renderer.NodeRenderer {
|
||||
return &Renderer{}
|
||||
}
|
||||
|
||||
// RegisterFuncs registers node rendering functions.
|
||||
func (r *Renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
// Blocks
|
||||
reg.Register(ast.KindDocument, r.renderDocument)
|
||||
reg.Register(ast.KindHeading, r.renderHeading)
|
||||
reg.Register(ast.KindParagraph, r.renderParagraph)
|
||||
reg.Register(ast.KindThematicBreak, r.renderThematicBreak)
|
||||
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
|
||||
reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
|
||||
reg.Register(ast.KindBlockquote, r.renderBlockquote)
|
||||
reg.Register(ast.KindList, r.renderList)
|
||||
reg.Register(ast.KindListItem, r.renderListItem)
|
||||
reg.Register(ast.KindImage, r.renderImage)
|
||||
|
||||
// Inlines
|
||||
reg.Register(ast.KindText, r.renderText)
|
||||
reg.Register(ast.KindEmphasis, r.renderEmphasis)
|
||||
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
|
||||
reg.Register(ast.KindLink, r.renderLink)
|
||||
|
||||
// Extensions
|
||||
reg.Register(extensionast.KindStrikethrough, r.renderStrikethrough)
|
||||
reg.Register(extensionast.KindTable, r.renderTable)
|
||||
reg.Register(extensionast.KindTableHeader, r.renderTableHeader)
|
||||
reg.Register(extensionast.KindTableRow, r.renderTableRow)
|
||||
reg.Register(extensionast.KindTableCell, r.renderTableCell)
|
||||
reg.Register(extensionast.KindTaskCheckBox, r.renderTaskCheckBox)
|
||||
}
|
||||
|
||||
// inRichTextMode returns true when we're inside a list or table context
|
||||
// in slack blockkit list and table items are rendered as rich_text elements
|
||||
// if more cases are found in future those needs to be added here.
|
||||
func (r *Renderer) inRichTextMode() bool {
|
||||
return r.listCtx != nil || r.tableCtx != nil
|
||||
}
|
||||
|
||||
// currentStyle merges the stored style stack into RichTextStyle
|
||||
// which can be applied on rich text elements.
|
||||
func (r *Renderer) currentStyle() *RichTextStyle {
|
||||
s := RichTextStyle{}
|
||||
for _, f := range r.styleStack {
|
||||
s.Bold = s.Bold || f.Bold
|
||||
s.Italic = s.Italic || f.Italic
|
||||
s.Strike = s.Strike || f.Strike
|
||||
s.Code = s.Code || f.Code
|
||||
}
|
||||
if s == (RichTextStyle{}) {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
// flushMrkdwn collects markdown text and adds it as a SectionBlock with mrkdwn text
|
||||
// whenever starting a new block we flush markdown to render it as a separate block.
|
||||
func (r *Renderer) flushMrkdwn() {
|
||||
text := strings.TrimSpace(r.mrkdwn.String())
|
||||
if text != "" {
|
||||
r.blocks = append(r.blocks, SectionBlock{
|
||||
Type: "section",
|
||||
Text: &TextObject{
|
||||
Type: "mrkdwn",
|
||||
Text: text,
|
||||
},
|
||||
})
|
||||
}
|
||||
r.mrkdwn.Reset()
|
||||
}
|
||||
|
||||
// addInline adds an inline element to the appropriate context.
|
||||
func (r *Renderer) addInline(el interface{}) {
|
||||
if r.listCtx != nil {
|
||||
r.listCtx.currentItemInlines = append(r.listCtx.currentItemInlines, el)
|
||||
} else if r.tableCtx != nil {
|
||||
r.tableCtx.currentCellInlines = append(r.tableCtx.currentCellInlines, el)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Document ---
|
||||
|
||||
func (r *Renderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
r.blocks = nil
|
||||
r.mrkdwn.Reset()
|
||||
r.styleStack = nil
|
||||
r.listCtx = nil
|
||||
r.tableCtx = nil
|
||||
r.blockquoteDepth = 0
|
||||
} else {
|
||||
// on exiting the document node write the json for the collected blocks.
|
||||
r.flushMrkdwn()
|
||||
var data []byte
|
||||
var err error
|
||||
if len(r.blocks) > 0 {
|
||||
data, err = json.Marshal(r.blocks)
|
||||
if err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
} else {
|
||||
// if no blocks are collected, write an empty array.
|
||||
data = []byte("[]")
|
||||
}
|
||||
_, err = w.Write(data)
|
||||
if err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// --- Heading ---
|
||||
|
||||
func (r *Renderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
r.mrkdwn.WriteString("*")
|
||||
} else {
|
||||
r.mrkdwn.WriteString("*\n")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// --- Paragraph ---
|
||||
|
||||
func (r *Renderer) renderParagraph(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
if r.mrkdwn.Len() > 0 {
|
||||
text := r.mrkdwn.String()
|
||||
if !strings.HasSuffix(text, "\n") {
|
||||
r.mrkdwn.WriteString("\n")
|
||||
}
|
||||
}
|
||||
// handling of nested blockquotes
|
||||
if r.blockquoteDepth > 0 {
|
||||
r.mrkdwn.WriteString(strings.Repeat("> ", r.blockquoteDepth))
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// --- ThematicBreak ---
|
||||
|
||||
func (r *Renderer) renderThematicBreak(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
r.flushMrkdwn()
|
||||
r.blocks = append(r.blocks, DividerBlock{Type: "divider"})
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// --- CodeBlock (indented) ---
|
||||
|
||||
func (r *Renderer) renderCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
r.flushMrkdwn()
|
||||
|
||||
var buf bytes.Buffer
|
||||
lines := node.Lines()
|
||||
for i := 0; i < lines.Len(); i++ {
|
||||
line := lines.At(i)
|
||||
buf.Write(line.Value(source))
|
||||
}
|
||||
|
||||
text := buf.String()
|
||||
// Remove trailing newline
|
||||
text = strings.TrimRight(text, "\n")
|
||||
// Slack API rejects empty text in rich_text_preformatted elements
|
||||
if text == "" {
|
||||
text = " "
|
||||
}
|
||||
|
||||
elements := []interface{}{
|
||||
RichTextInline{Type: "text", Text: text},
|
||||
}
|
||||
|
||||
r.blocks = append(r.blocks, RichTextBlock{
|
||||
Type: "rich_text",
|
||||
Elements: []interface{}{
|
||||
RichTextPreformatted{
|
||||
Type: "rich_text_preformatted",
|
||||
Elements: elements,
|
||||
Border: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// --- FencedCodeBlock ---
|
||||
|
||||
func (r *Renderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
r.flushMrkdwn()
|
||||
|
||||
n := node.(*ast.FencedCodeBlock)
|
||||
var buf bytes.Buffer
|
||||
lines := node.Lines()
|
||||
for i := 0; i < lines.Len(); i++ {
|
||||
line := lines.At(i)
|
||||
buf.Write(line.Value(source))
|
||||
}
|
||||
|
||||
text := buf.String()
|
||||
text = strings.TrimRight(text, "\n")
|
||||
// Slack API rejects empty text in rich_text_preformatted elements
|
||||
if text == "" {
|
||||
text = " "
|
||||
}
|
||||
|
||||
elements := []interface{}{
|
||||
RichTextInline{Type: "text", Text: text},
|
||||
}
|
||||
|
||||
// If language is specified, collect it.
|
||||
var language string
|
||||
lang := n.Language(source)
|
||||
if len(lang) > 0 {
|
||||
language = string(lang)
|
||||
}
|
||||
// Add the preformatted block to the blocks slice with the collected language.
|
||||
r.blocks = append(r.blocks, RichTextBlock{
|
||||
Type: "rich_text",
|
||||
Elements: []interface{}{
|
||||
RichTextPreformatted{
|
||||
Type: "rich_text_preformatted",
|
||||
Elements: elements,
|
||||
Border: 0,
|
||||
Language: language,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
||||
// --- Blockquote ---
|
||||
|
||||
func (r *Renderer) renderBlockquote(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
r.blockquoteDepth++
|
||||
} else {
|
||||
r.blockquoteDepth--
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// --- List ---
|
||||
|
||||
func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
list := node.(*ast.List)
|
||||
|
||||
if entering {
|
||||
style := "bullet"
|
||||
if list.IsOrdered() {
|
||||
style = "ordered"
|
||||
}
|
||||
|
||||
if r.listCtx == nil {
|
||||
// Top-level list: flush mrkdwn and create context
|
||||
r.flushMrkdwn()
|
||||
r.listCtx = &listContext{}
|
||||
} else {
|
||||
// Nested list: check if we already have some collected list items that needs to be flushed.
|
||||
// in slack blockkit, list items with different levels of indentation are added as different rich_text_list blocks.
|
||||
if len(r.listCtx.currentItemInlines) > 0 {
|
||||
sec := RichTextBlock{
|
||||
Type: "rich_text_section",
|
||||
Elements: r.listCtx.currentItemInlines,
|
||||
}
|
||||
if r.listCtx.current != nil {
|
||||
r.listCtx.current.Elements = append(r.listCtx.current.Elements, sec)
|
||||
}
|
||||
r.listCtx.currentItemInlines = nil
|
||||
// Increment parent's itemCount
|
||||
if len(r.listCtx.stack) > 0 {
|
||||
r.listCtx.stack[len(r.listCtx.stack)-1].itemCount++
|
||||
}
|
||||
}
|
||||
// Finalize current list to result only if items were collected
|
||||
if r.listCtx.current != nil && len(r.listCtx.current.Elements) > 0 {
|
||||
r.listCtx.result = append(r.listCtx.result, *r.listCtx.current)
|
||||
}
|
||||
}
|
||||
|
||||
// the stack accumulated till this level derives hte indentation
|
||||
// the stack get's collected as we go in more nested levels of list
|
||||
// and as we get our of the nesting we remove the items from the slack
|
||||
indent := len(r.listCtx.stack)
|
||||
r.listCtx.stack = append(r.listCtx.stack, listFrame{
|
||||
style: style,
|
||||
indent: indent,
|
||||
itemCount: 0,
|
||||
})
|
||||
|
||||
newList := &RichTextList{
|
||||
Type: "rich_text_list",
|
||||
Style: style,
|
||||
Indent: indent,
|
||||
Border: 0,
|
||||
Elements: []interface{}{},
|
||||
}
|
||||
|
||||
// Handle ordered list with start > 1
|
||||
if list.IsOrdered() && list.Start > 1 {
|
||||
newList.Offset = list.Start - 1
|
||||
}
|
||||
|
||||
r.listCtx.current = newList
|
||||
|
||||
} else {
|
||||
// Leaving list: finalize current list
|
||||
if r.listCtx.current != nil && len(r.listCtx.current.Elements) > 0 {
|
||||
r.listCtx.result = append(r.listCtx.result, *r.listCtx.current)
|
||||
}
|
||||
|
||||
// Pop stack to so upcoming indentations can be handled correctly.
|
||||
r.listCtx.stack = r.listCtx.stack[:len(r.listCtx.stack)-1]
|
||||
|
||||
if len(r.listCtx.stack) > 0 {
|
||||
// Resume parent: start a new list segment at parent indent/style
|
||||
parent := &r.listCtx.stack[len(r.listCtx.stack)-1]
|
||||
newList := &RichTextList{
|
||||
Type: "rich_text_list",
|
||||
Style: parent.style,
|
||||
Indent: parent.indent,
|
||||
Border: 0,
|
||||
Elements: []interface{}{},
|
||||
}
|
||||
// Set offset for ordered parent continuation
|
||||
if parent.style == "ordered" && parent.itemCount > 0 {
|
||||
newList.Offset = parent.itemCount
|
||||
}
|
||||
r.listCtx.current = newList
|
||||
} else {
|
||||
// Top-level list is done since all stack are popped: build RichTextBlock if non-empty
|
||||
if len(r.listCtx.result) > 0 {
|
||||
elements := make([]interface{}, len(r.listCtx.result))
|
||||
for i, l := range r.listCtx.result {
|
||||
elements[i] = l
|
||||
}
|
||||
r.blocks = append(r.blocks, RichTextBlock{
|
||||
Type: "rich_text",
|
||||
Elements: elements,
|
||||
})
|
||||
}
|
||||
r.listCtx = nil
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// --- ListItem ---
|
||||
|
||||
func (r *Renderer) renderListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
r.listCtx.currentItemInlines = nil
|
||||
} else {
|
||||
// Only add if there are inlines (might be empty after nested list consumed them)
|
||||
if len(r.listCtx.currentItemInlines) > 0 {
|
||||
sec := RichTextBlock{
|
||||
Type: "rich_text_section",
|
||||
Elements: r.listCtx.currentItemInlines,
|
||||
}
|
||||
if r.listCtx.current != nil {
|
||||
r.listCtx.current.Elements = append(r.listCtx.current.Elements, sec)
|
||||
}
|
||||
r.listCtx.currentItemInlines = nil
|
||||
// Increment parent frame's itemCount
|
||||
if len(r.listCtx.stack) > 0 {
|
||||
r.listCtx.stack[len(r.listCtx.stack)-1].itemCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// --- Table ---
|
||||
// when table is encountered, we flush the markdown and create a table context.
|
||||
// when header row is encountered, we set the isHeader flag to true
|
||||
// when each row ends in renderTableRow we add that row to rows array of table context.
|
||||
// when table cell is encountered, we apply header related styles to the collected inline items,
|
||||
// all inline items are parsed as separate AST items like list item, links, text, etc. are collected
|
||||
// using the addInline function and wrapped in a rich_text_section block.
|
||||
|
||||
func (r *Renderer) renderTable(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
r.flushMrkdwn()
|
||||
r.tableCtx = &tableContext{}
|
||||
} else {
|
||||
// Pad short rows to match header column count for valid Block Kit payload
|
||||
// without this slack blockkit attachment is invalid and the API fails
|
||||
rows := r.tableCtx.rows
|
||||
if len(rows) > 0 {
|
||||
maxCols := len(rows[0])
|
||||
for i, row := range rows {
|
||||
for len(row) < maxCols {
|
||||
emptySec := RichTextBlock{
|
||||
Type: "rich_text_section",
|
||||
Elements: []interface{}{RichTextInline{Type: "text", Text: " "}},
|
||||
}
|
||||
row = append(row, TableCell{
|
||||
Type: "rich_text",
|
||||
Elements: []interface{}{emptySec},
|
||||
})
|
||||
}
|
||||
rows[i] = row
|
||||
}
|
||||
}
|
||||
r.blocks = append(r.blocks, TableBlock{
|
||||
Type: "table",
|
||||
Rows: rows,
|
||||
})
|
||||
r.tableCtx = nil
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderTableHeader(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
r.tableCtx.isHeader = true
|
||||
r.tableCtx.currentRow = nil
|
||||
} else {
|
||||
r.tableCtx.rows = append(r.tableCtx.rows, r.tableCtx.currentRow)
|
||||
r.tableCtx.currentRow = nil
|
||||
r.tableCtx.isHeader = false
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderTableRow(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
r.tableCtx.currentRow = nil
|
||||
} else {
|
||||
r.tableCtx.rows = append(r.tableCtx.rows, r.tableCtx.currentRow)
|
||||
r.tableCtx.currentRow = nil
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderTableCell(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
r.tableCtx.currentCellInlines = nil
|
||||
} else {
|
||||
// If header, make text bold for the collected inline items.
|
||||
if r.tableCtx.isHeader {
|
||||
for i, el := range r.tableCtx.currentCellInlines {
|
||||
if inline, ok := el.(RichTextInline); ok {
|
||||
if inline.Style == nil {
|
||||
inline.Style = &RichTextStyle{Bold: true}
|
||||
} else {
|
||||
inline.Style.Bold = true
|
||||
}
|
||||
r.tableCtx.currentCellInlines[i] = inline
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ensure cell has at least one element for valid Block Kit payload
|
||||
if len(r.tableCtx.currentCellInlines) == 0 {
|
||||
r.tableCtx.currentCellInlines = []interface{}{
|
||||
RichTextInline{Type: "text", Text: " "},
|
||||
}
|
||||
}
|
||||
// All inline items that are collected for a table cell are wrapped in a rich_text_section block.
|
||||
sec := RichTextBlock{
|
||||
Type: "rich_text_section",
|
||||
Elements: r.tableCtx.currentCellInlines,
|
||||
}
|
||||
// The rich_text_section block is wrapped in a rich_text block.
|
||||
cell := TableCell{
|
||||
Type: "rich_text",
|
||||
Elements: []interface{}{sec},
|
||||
}
|
||||
r.tableCtx.currentRow = append(r.tableCtx.currentRow, cell)
|
||||
r.tableCtx.currentCellInlines = nil
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// --- TaskCheckBox ---
|
||||
|
||||
func (r *Renderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
n := node.(*extensionast.TaskCheckBox)
|
||||
text := "[ ] "
|
||||
if n.IsChecked {
|
||||
text = "[x] "
|
||||
}
|
||||
if r.inRichTextMode() {
|
||||
r.addInline(RichTextInline{Type: "text", Text: text})
|
||||
} else {
|
||||
r.mrkdwn.WriteString(text)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// --- Inline: Text ---
|
||||
|
||||
func (r *Renderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
n := node.(*ast.Text)
|
||||
value := string(n.Segment.Value(source))
|
||||
|
||||
if r.inRichTextMode() {
|
||||
r.addInline(RichTextInline{
|
||||
Type: "text",
|
||||
Text: value,
|
||||
Style: r.currentStyle(),
|
||||
})
|
||||
if n.HardLineBreak() || n.SoftLineBreak() {
|
||||
r.addInline(RichTextInline{Type: "text", Text: "\n"})
|
||||
}
|
||||
} else {
|
||||
r.mrkdwn.WriteString(value)
|
||||
if n.HardLineBreak() || n.SoftLineBreak() {
|
||||
r.mrkdwn.WriteString("\n")
|
||||
if r.blockquoteDepth > 0 {
|
||||
r.mrkdwn.WriteString(strings.Repeat("> ", r.blockquoteDepth))
|
||||
}
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// --- Inline: Emphasis ---
|
||||
|
||||
func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.Emphasis)
|
||||
if r.inRichTextMode() {
|
||||
if entering {
|
||||
s := RichTextStyle{}
|
||||
if n.Level == 1 {
|
||||
s.Italic = true
|
||||
} else {
|
||||
s.Bold = true
|
||||
}
|
||||
r.styleStack = append(r.styleStack, s)
|
||||
} else {
|
||||
// the collected style gets used by the rich text element using currentStyle()
|
||||
// so we remove this style from the stack.
|
||||
if len(r.styleStack) > 0 {
|
||||
r.styleStack = r.styleStack[:len(r.styleStack)-1]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if n.Level == 1 {
|
||||
r.mrkdwn.WriteString("_")
|
||||
} else {
|
||||
r.mrkdwn.WriteString("*")
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// --- Inline: Strikethrough ---
|
||||
|
||||
func (r *Renderer) renderStrikethrough(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if r.inRichTextMode() {
|
||||
if entering {
|
||||
r.styleStack = append(r.styleStack, RichTextStyle{Strike: true})
|
||||
} else {
|
||||
// the collected style gets used by the rich text element using currentStyle()
|
||||
// so we remove this style from the stack.
|
||||
if len(r.styleStack) > 0 {
|
||||
r.styleStack = r.styleStack[:len(r.styleStack)-1]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
r.mrkdwn.WriteString("~")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// --- Inline: CodeSpan ---
|
||||
|
||||
func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
if r.inRichTextMode() {
|
||||
// Collect all child text
|
||||
var buf bytes.Buffer
|
||||
for c := node.FirstChild(); c != nil; c = c.NextSibling() {
|
||||
if t, ok := c.(*ast.Text); ok {
|
||||
v := t.Segment.Value(source)
|
||||
if bytes.HasSuffix(v, []byte("\n")) {
|
||||
buf.Write(v[:len(v)-1])
|
||||
buf.WriteByte(' ')
|
||||
} else {
|
||||
buf.Write(v)
|
||||
}
|
||||
} else if s, ok := c.(*ast.String); ok {
|
||||
buf.Write(s.Value)
|
||||
}
|
||||
}
|
||||
style := r.currentStyle()
|
||||
if style == nil {
|
||||
style = &RichTextStyle{Code: true}
|
||||
} else {
|
||||
style.Code = true
|
||||
}
|
||||
r.addInline(RichTextInline{
|
||||
Type: "text",
|
||||
Text: buf.String(),
|
||||
Style: style,
|
||||
})
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
// mrkdwn mode
|
||||
r.mrkdwn.WriteByte('`')
|
||||
for c := node.FirstChild(); c != nil; c = c.NextSibling() {
|
||||
if t, ok := c.(*ast.Text); ok {
|
||||
v := t.Segment.Value(source)
|
||||
if bytes.HasSuffix(v, []byte("\n")) {
|
||||
r.mrkdwn.Write(v[:len(v)-1])
|
||||
r.mrkdwn.WriteByte(' ')
|
||||
} else {
|
||||
r.mrkdwn.Write(v)
|
||||
}
|
||||
} else if s, ok := c.(*ast.String); ok {
|
||||
r.mrkdwn.Write(s.Value)
|
||||
}
|
||||
}
|
||||
r.mrkdwn.WriteByte('`')
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
||||
// --- Inline: Link ---
|
||||
|
||||
func (r *Renderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.Link)
|
||||
if r.inRichTextMode() {
|
||||
if entering {
|
||||
// Walk the entire subtree to collect text from all descendants,
|
||||
// including nested inline nodes like emphasis, strong, code spans, etc.
|
||||
var buf bytes.Buffer
|
||||
_ = ast.Walk(node, func(child ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering || child == node {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
if t, ok := child.(*ast.Text); ok {
|
||||
buf.Write(t.Segment.Value(source))
|
||||
} else if s, ok := child.(*ast.String); ok {
|
||||
buf.Write(s.Value)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
})
|
||||
// Once we've collected the text for the link (given it was present)
|
||||
// let's add the link to the rich text block.
|
||||
r.addInline(RichTextLink{
|
||||
Type: "link",
|
||||
URL: string(n.Destination),
|
||||
Text: buf.String(),
|
||||
Style: r.currentStyle(),
|
||||
})
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
} else {
|
||||
if entering {
|
||||
r.mrkdwn.WriteString("<")
|
||||
r.mrkdwn.Write(n.Destination)
|
||||
r.mrkdwn.WriteString("|")
|
||||
} else {
|
||||
r.mrkdwn.WriteString(">")
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// --- Image (skip) ---
|
||||
|
||||
func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package slackblockkitrenderer
|
||||
|
||||
// SectionBlock represents a Slack section block with mrkdwn text.
|
||||
type SectionBlock struct {
|
||||
Type string `json:"type"`
|
||||
Text *TextObject `json:"text"`
|
||||
}
|
||||
|
||||
// DividerBlock represents a Slack divider block.
|
||||
type DividerBlock struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// RichTextBlock is a container for rich text elements (lists, code blocks, table and cell blocks).
|
||||
type RichTextBlock struct {
|
||||
Type string `json:"type"`
|
||||
Elements []interface{} `json:"elements"`
|
||||
}
|
||||
|
||||
// TableBlock represents a Slack table rendered as a rich_text block with preformatted text.
|
||||
type TableBlock struct {
|
||||
Type string `json:"type"`
|
||||
Rows [][]TableCell `json:"rows"`
|
||||
}
|
||||
|
||||
// TableCell is a cell in a table block.
|
||||
type TableCell struct {
|
||||
Type string `json:"type"`
|
||||
Elements []interface{} `json:"elements"`
|
||||
}
|
||||
|
||||
// TextObject is the text field inside a SectionBlock.
|
||||
type TextObject struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// RichTextList represents an ordered or unordered list.
|
||||
type RichTextList struct {
|
||||
Type string `json:"type"`
|
||||
Style string `json:"style"`
|
||||
Indent int `json:"indent"`
|
||||
Border int `json:"border"`
|
||||
Offset int `json:"offset,omitempty"`
|
||||
Elements []interface{} `json:"elements"`
|
||||
}
|
||||
|
||||
// RichTextPreformatted represents a code block.
|
||||
type RichTextPreformatted struct {
|
||||
Type string `json:"type"`
|
||||
Elements []interface{} `json:"elements"`
|
||||
Border int `json:"border"`
|
||||
Language string `json:"language,omitempty"`
|
||||
}
|
||||
|
||||
// RichTextInline represents inline text with optional styling
|
||||
// ex: text inside list, table cell
|
||||
type RichTextInline struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
Style *RichTextStyle `json:"style,omitempty"`
|
||||
}
|
||||
|
||||
// RichTextLink represents a link inside rich text
|
||||
// ex: link inside list, table cell
|
||||
type RichTextLink struct {
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Style *RichTextStyle `json:"style,omitempty"`
|
||||
}
|
||||
|
||||
// RichTextStyle holds boolean style flags for inline elements
|
||||
// these bools can toggle different styles for a rich text element at once.
|
||||
type RichTextStyle struct {
|
||||
Bold bool `json:"bold,omitempty"`
|
||||
Italic bool `json:"italic,omitempty"`
|
||||
Strike bool `json:"strike,omitempty"`
|
||||
Code bool `json:"code,omitempty"`
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package slackmrkdwnrenderer
|
||||
|
||||
import (
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
type slackMrkdwn struct{}
|
||||
|
||||
// SlackMrkdwn is a goldmark.Extender that configures the Slack mrkdwn renderer.
|
||||
var SlackMrkdwn = &slackMrkdwn{}
|
||||
|
||||
// Extend implements goldmark.Extender.
|
||||
func (e *slackMrkdwn) Extend(m goldmark.Markdown) {
|
||||
extension.Table.Extend(m)
|
||||
extension.Strikethrough.Extend(m)
|
||||
m.Renderer().AddOptions(
|
||||
renderer.WithNodeRenderers(util.Prioritized(NewRenderer(), 1)),
|
||||
)
|
||||
}
|
||||
@@ -1,383 +0,0 @@
|
||||
package slackmrkdwnrenderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
extensionast "github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
// Renderer renders nodes as Slack mrkdwn.
|
||||
type Renderer struct {
|
||||
prefixes []string
|
||||
}
|
||||
|
||||
// NewRenderer returns a new Renderer with given options.
|
||||
func NewRenderer() renderer.NodeRenderer {
|
||||
return &Renderer{}
|
||||
}
|
||||
|
||||
// RegisterFuncs implements NodeRenderer.RegisterFuncs.
|
||||
func (r *Renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
// Blocks
|
||||
reg.Register(ast.KindDocument, r.renderDocument)
|
||||
reg.Register(ast.KindHeading, r.renderHeading)
|
||||
reg.Register(ast.KindBlockquote, r.renderBlockquote)
|
||||
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
|
||||
reg.Register(ast.KindFencedCodeBlock, r.renderCodeBlock)
|
||||
reg.Register(ast.KindList, r.renderList)
|
||||
reg.Register(ast.KindListItem, r.renderListItem)
|
||||
reg.Register(ast.KindParagraph, r.renderParagraph)
|
||||
reg.Register(ast.KindTextBlock, r.renderTextBlock)
|
||||
reg.Register(ast.KindRawHTML, r.renderRawHTML)
|
||||
reg.Register(ast.KindThematicBreak, r.renderThematicBreak)
|
||||
|
||||
// Inlines
|
||||
reg.Register(ast.KindAutoLink, r.renderAutoLink)
|
||||
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
|
||||
reg.Register(ast.KindEmphasis, r.renderEmphasis)
|
||||
reg.Register(ast.KindImage, r.renderImage)
|
||||
reg.Register(ast.KindLink, r.renderLink)
|
||||
reg.Register(ast.KindText, r.renderText)
|
||||
|
||||
// Extensions
|
||||
reg.Register(extensionast.KindStrikethrough, r.renderStrikethrough)
|
||||
reg.Register(extensionast.KindTable, r.renderTable)
|
||||
}
|
||||
|
||||
func (r *Renderer) writePrefix(w util.BufWriter) {
|
||||
for _, p := range r.prefixes {
|
||||
_, _ = w.WriteString(p)
|
||||
}
|
||||
}
|
||||
|
||||
// writeLineSeparator writes a newline followed by the current prefix.
|
||||
// Used for tight separations (e.g., between list items or text blocks).
|
||||
func (r *Renderer) writeLineSeparator(w util.BufWriter) {
|
||||
_ = w.WriteByte('\n')
|
||||
r.writePrefix(w)
|
||||
}
|
||||
|
||||
// writeBlockSeparator writes a blank line separator between block-level elements,
|
||||
// respecting any active prefixes for proper nesting (e.g., inside blockquotes).
|
||||
func (r *Renderer) writeBlockSeparator(w util.BufWriter) {
|
||||
r.writeLineSeparator(w)
|
||||
r.writeLineSeparator(w)
|
||||
}
|
||||
|
||||
// separateFromPrevious writes a block separator if the node has a previous sibling.
|
||||
func (r *Renderer) separateFromPrevious(w util.BufWriter, n ast.Node) {
|
||||
if n.PreviousSibling() != nil {
|
||||
r.writeBlockSeparator(w)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
_, _ = w.WriteString("\n\n")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
r.separateFromPrevious(w, node)
|
||||
}
|
||||
_, _ = w.WriteString("*")
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderBlockquote(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
r.separateFromPrevious(w, n)
|
||||
r.prefixes = append(r.prefixes, "> ")
|
||||
_, _ = w.WriteString("> ")
|
||||
} else {
|
||||
r.prefixes = r.prefixes[:len(r.prefixes)-1]
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderCodeBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
r.separateFromPrevious(w, n)
|
||||
// start code block and write code line by line
|
||||
_, _ = w.WriteString("```\n")
|
||||
l := n.Lines().Len()
|
||||
for i := 0; i < l; i++ {
|
||||
line := n.Lines().At(i)
|
||||
v := line.Value(source)
|
||||
_, _ = w.Write(v)
|
||||
}
|
||||
} else {
|
||||
_, _ = w.WriteString("```")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
if node.PreviousSibling() != nil {
|
||||
r.writeLineSeparator(w)
|
||||
// another line break if not a nested list item and starting another block
|
||||
if node.Parent() == nil || node.Parent().Kind() != ast.KindListItem {
|
||||
r.writeLineSeparator(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderListItem(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
if n.PreviousSibling() != nil {
|
||||
r.writeLineSeparator(w)
|
||||
}
|
||||
parent := n.Parent().(*ast.List)
|
||||
// compute and write the prefix based on list type and index
|
||||
var prefixStr string
|
||||
if parent.IsOrdered() {
|
||||
index := parent.Start
|
||||
for c := parent.FirstChild(); c != nil && c != n; c = c.NextSibling() {
|
||||
index++
|
||||
}
|
||||
prefixStr = fmt.Sprintf("%d. ", index)
|
||||
} else {
|
||||
prefixStr = "• "
|
||||
}
|
||||
_, _ = w.WriteString(prefixStr)
|
||||
r.prefixes = append(r.prefixes, "\t") // add tab for nested list items
|
||||
} else {
|
||||
r.prefixes = r.prefixes[:len(r.prefixes)-1]
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderParagraph(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
r.separateFromPrevious(w, n)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderTextBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering && n.PreviousSibling() != nil {
|
||||
r.writeLineSeparator(w)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderRawHTML(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
n := n.(*ast.RawHTML)
|
||||
l := n.Segments.Len()
|
||||
for i := 0; i < l; i++ {
|
||||
segment := n.Segments.At(i)
|
||||
_, _ = w.Write(segment.Value(source))
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderThematicBreak(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
r.separateFromPrevious(w, n)
|
||||
_, _ = w.WriteString("---")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
n := node.(*ast.AutoLink)
|
||||
url := string(n.URL(source))
|
||||
label := string(n.Label(source))
|
||||
|
||||
if n.AutoLinkType == ast.AutoLinkEmail && !strings.HasPrefix(strings.ToLower(url), "mailto:") {
|
||||
url = "mailto:" + url
|
||||
}
|
||||
|
||||
if url == label {
|
||||
_, _ = fmt.Fprintf(w, "<%s>", url)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(w, "<%s|%s>", url, label)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
_ = w.WriteByte('`')
|
||||
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
||||
segment := c.(*ast.Text).Segment
|
||||
value := segment.Value(source)
|
||||
if bytes.HasSuffix(value, []byte("\n")) { // replace newline with space
|
||||
_, _ = w.Write(value[:len(value)-1])
|
||||
_ = w.WriteByte(' ')
|
||||
} else {
|
||||
_, _ = w.Write(value)
|
||||
}
|
||||
}
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
_ = w.WriteByte('`')
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.Emphasis)
|
||||
mark := "_"
|
||||
if n.Level == 2 {
|
||||
mark = "*"
|
||||
}
|
||||
_, _ = w.WriteString(mark)
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.Link)
|
||||
if entering {
|
||||
_, _ = w.WriteString("<")
|
||||
_, _ = w.Write(util.URLEscape(n.Destination, true))
|
||||
_, _ = w.WriteString("|")
|
||||
} else {
|
||||
_, _ = w.WriteString(">")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
n := node.(*ast.Image)
|
||||
_, _ = w.WriteString("<")
|
||||
_, _ = w.Write(util.URLEscape(n.Destination, true))
|
||||
_, _ = w.WriteString("|")
|
||||
|
||||
// Write the alt text directly
|
||||
var altBuf bytes.Buffer
|
||||
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
||||
if textNode, ok := c.(*ast.Text); ok {
|
||||
altBuf.Write(textNode.Segment.Value(source))
|
||||
}
|
||||
}
|
||||
_, _ = w.Write(altBuf.Bytes())
|
||||
|
||||
_, _ = w.WriteString(">")
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
n := node.(*ast.Text)
|
||||
segment := n.Segment
|
||||
value := segment.Value(source)
|
||||
_, _ = w.Write(value)
|
||||
if n.HardLineBreak() || n.SoftLineBreak() {
|
||||
r.writeLineSeparator(w)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderStrikethrough(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
_, _ = w.WriteString("~")
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderTable(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
r.separateFromPrevious(w, node)
|
||||
|
||||
// Collect cells and max widths
|
||||
var rows [][]string
|
||||
var colWidths []int
|
||||
|
||||
for c := node.FirstChild(); c != nil; c = c.NextSibling() {
|
||||
if c.Kind() == extensionast.KindTableHeader || c.Kind() == extensionast.KindTableRow {
|
||||
var row []string
|
||||
colIdx := 0
|
||||
for cc := c.FirstChild(); cc != nil; cc = cc.NextSibling() {
|
||||
if cc.Kind() == extensionast.KindTableCell {
|
||||
cellText := extractPlainText(cc, source)
|
||||
row = append(row, cellText)
|
||||
runeLen := utf8.RuneCountInString(cellText)
|
||||
if colIdx >= len(colWidths) {
|
||||
colWidths = append(colWidths, runeLen)
|
||||
} else if runeLen > colWidths[colIdx] {
|
||||
colWidths[colIdx] = runeLen
|
||||
}
|
||||
colIdx++
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
}
|
||||
|
||||
// writing table in code block
|
||||
_, _ = w.WriteString("```\n")
|
||||
for i, row := range rows {
|
||||
for colIdx, cellText := range row {
|
||||
width := 0
|
||||
if colIdx < len(colWidths) {
|
||||
width = colWidths[colIdx]
|
||||
}
|
||||
runeLen := utf8.RuneCountInString(cellText)
|
||||
padding := max(0, width-runeLen)
|
||||
|
||||
_, _ = w.WriteString(cellText)
|
||||
_, _ = w.WriteString(strings.Repeat(" ", padding))
|
||||
if colIdx < len(row)-1 {
|
||||
_, _ = w.WriteString(" | ")
|
||||
}
|
||||
}
|
||||
_ = w.WriteByte('\n')
|
||||
|
||||
// Print separator after header
|
||||
if i == 0 {
|
||||
for colIdx := range row {
|
||||
width := 0
|
||||
if colIdx < len(colWidths) {
|
||||
width = colWidths[colIdx]
|
||||
}
|
||||
_, _ = w.WriteString(strings.Repeat("-", width))
|
||||
if colIdx < len(row)-1 {
|
||||
_, _ = w.WriteString("-|-")
|
||||
}
|
||||
}
|
||||
_ = w.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
_, _ = w.WriteString("```")
|
||||
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
||||
// extractPlainText extracts all the text content from the given node.
|
||||
func extractPlainText(n ast.Node, source []byte) string {
|
||||
var buf bytes.Buffer
|
||||
_ = ast.Walk(n, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
if textNode, ok := node.(*ast.Text); ok {
|
||||
buf.Write(textNode.Segment.Value(source))
|
||||
} else if strNode, ok := node.(*ast.String); ok {
|
||||
buf.Write(strNode.Value)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
})
|
||||
return strings.TrimSpace(buf.String())
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package slackmrkdwnrenderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
)
|
||||
|
||||
func TestRenderer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
markdown string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Heading with Thematic Break",
|
||||
markdown: "# Title 1\n# Hello World\n---\nthis is sometext",
|
||||
expected: "*Title 1*\n\n*Hello World*\n\n---\n\nthis is sometext\n\n",
|
||||
},
|
||||
{
|
||||
name: "Blockquote",
|
||||
markdown: "> This is a quote\n> It continues",
|
||||
expected: "> This is a quote\n> It continues\n\n",
|
||||
},
|
||||
{
|
||||
name: "Fenced Code Block",
|
||||
markdown: "```go\npackage main\nfunc main() {}\n```",
|
||||
expected: "```\npackage main\nfunc main() {}\n```\n\n",
|
||||
},
|
||||
{
|
||||
name: "Unordered List",
|
||||
markdown: "- item 1\n- item 2\n- item 3",
|
||||
expected: "• item 1\n• item 2\n• item 3\n\n",
|
||||
},
|
||||
{
|
||||
name: "nested unordered list",
|
||||
markdown: "- item 1\n- item 2\n\t- item 2.1\n\t\t- item 2.1.1\n\t\t- item 2.1.2\n\t- item 2.2\n- item 3",
|
||||
expected: "• item 1\n• item 2\n\t• item 2.1\n\t\t• item 2.1.1\n\t\t• item 2.1.2\n\t• item 2.2\n• item 3\n\n",
|
||||
},
|
||||
{
|
||||
name: "Ordered List",
|
||||
markdown: "1. item 1\n2. item 2\n3. item 3",
|
||||
expected: "1. item 1\n2. item 2\n3. item 3\n\n",
|
||||
},
|
||||
{
|
||||
name: "nested ordered list",
|
||||
markdown: "1. item 1\n2. item 2\n\t1. item 2.1\n\t\t1. item 2.1.1\n\t\t2. item 2.1.2\n\t2. item 2.2\n\t3. item 2.3\n3. item 3\n4. item 4",
|
||||
expected: "1. item 1\n2. item 2\n\t1. item 2.1\n\t\t1. item 2.1.1\n\t\t2. item 2.1.2\n\t2. item 2.2\n\t3. item 2.3\n3. item 3\n4. item 4\n\n",
|
||||
},
|
||||
{
|
||||
name: "Links and AutoLinks",
|
||||
markdown: "This is a [link](https://example.com) and an autolink <https://test.com>",
|
||||
expected: "This is a <https://example.com|link> and an autolink <https://test.com>\n\n",
|
||||
},
|
||||
{
|
||||
name: "Images",
|
||||
markdown: "An image ",
|
||||
expected: "An image <https://example.com/image.png|alt text>\n\n",
|
||||
},
|
||||
{
|
||||
name: "Emphasis",
|
||||
markdown: "This is **bold** and *italic* and __bold__ and _italic_",
|
||||
expected: "This is *bold* and _italic_ and *bold* and _italic_\n\n",
|
||||
},
|
||||
{
|
||||
name: "Strikethrough",
|
||||
markdown: "This is ~~strike~~",
|
||||
expected: "This is ~strike~\n\n",
|
||||
},
|
||||
{
|
||||
name: "Code Span",
|
||||
markdown: "This is `inline code` embedded.",
|
||||
expected: "This is `inline code` embedded.\n\n",
|
||||
},
|
||||
{
|
||||
name: "Table",
|
||||
markdown: "Col 1 | Col 2 | Col 3\n--- | --- | ---\nVal 1 | Long Value 2 | 3\nShort | V | 1000",
|
||||
expected: "```\nCol 1 | Col 2 | Col 3\n------|--------------|------\nVal 1 | Long Value 2 | 3 \nShort | V | 1000 \n```\n\n",
|
||||
},
|
||||
{
|
||||
name: "Mixed Nested Lists",
|
||||
markdown: "1. first\n\t- nested bullet\n\t- another bullet\n2. second",
|
||||
expected: "1. first\n\t• nested bullet\n\t• another bullet\n2. second\n\n",
|
||||
},
|
||||
{
|
||||
name: "Email AutoLink",
|
||||
markdown: "<user@example.com>",
|
||||
expected: "<mailto:user@example.com|user@example.com>\n\n",
|
||||
},
|
||||
{
|
||||
name: "No value string parsed as is",
|
||||
markdown: "Service: <no value>",
|
||||
expected: "Service: <no value>\n\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
md := goldmark.New(goldmark.WithExtensions(SlackMrkdwn))
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
|
||||
t.Fatalf("failed to convert: %v", err)
|
||||
}
|
||||
|
||||
// Do exact string matching
|
||||
actual := buf.String()
|
||||
if actual != tt.expected {
|
||||
t.Errorf("\nExpected:\n%q\nGot:\n%q\nRaw Expected:\n%s\nRaw Got:\n%s",
|
||||
tt.expected, actual, tt.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package templatingextensions
|
||||
|
||||
import (
|
||||
"github.com/yuin/goldmark"
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
// NoValueHTMLRenderer is a renderer.NodeRenderer implementation that
|
||||
// renders <no value> as escaped visible text instead of omitting it.
|
||||
type NoValueHTMLRenderer struct {
|
||||
html.Config
|
||||
}
|
||||
|
||||
// NewNoValueHTMLRenderer returns a new NoValueHTMLRenderer.
|
||||
func NewNoValueHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||
r := &NoValueHTMLRenderer{
|
||||
Config: html.NewConfig(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.SetHTMLOption(&r.Config)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
||||
func (r *NoValueHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(gast.KindRawHTML, r.renderRawHTML)
|
||||
}
|
||||
|
||||
func (r *NoValueHTMLRenderer) renderRawHTML(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return gast.WalkSkipChildren, nil
|
||||
}
|
||||
if r.Unsafe {
|
||||
n := node.(*gast.RawHTML)
|
||||
for i := 0; i < n.Segments.Len(); i++ {
|
||||
segment := n.Segments.At(i)
|
||||
_, _ = w.Write(segment.Value(source))
|
||||
}
|
||||
return gast.WalkSkipChildren, nil
|
||||
}
|
||||
n := node.(*gast.RawHTML)
|
||||
raw := string(n.Segments.Value(source))
|
||||
if raw == "<no value>" {
|
||||
_, _ = w.WriteString("<no value>")
|
||||
return gast.WalkSkipChildren, nil
|
||||
}
|
||||
_, _ = w.WriteString("<!-- raw HTML omitted -->")
|
||||
return gast.WalkSkipChildren, nil
|
||||
}
|
||||
|
||||
type escapeNoValue struct{}
|
||||
|
||||
// EscapeNoValue is an extension that renders <no value> as visible
|
||||
// escaped text instead of omitting it as raw HTML.
|
||||
var EscapeNoValue = &escapeNoValue{}
|
||||
|
||||
func (e *escapeNoValue) Extend(m goldmark.Markdown) {
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||
util.Prioritized(NewNoValueHTMLRenderer(), 500),
|
||||
))
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package templatingextensions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
)
|
||||
|
||||
func TestEscapeNoValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
markdown string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "plain text",
|
||||
markdown: "Service: <no value>",
|
||||
expected: "<p>Service: <no value></p>\n",
|
||||
},
|
||||
{
|
||||
name: "inside strong",
|
||||
markdown: "Service: **<no value>**",
|
||||
expected: "<p>Service: <strong><no value></strong></p>\n",
|
||||
},
|
||||
{
|
||||
name: "inside emphasis",
|
||||
markdown: "Service: *<no value>*",
|
||||
expected: "<p>Service: <em><no value></em></p>\n",
|
||||
},
|
||||
{
|
||||
name: "inside strikethrough",
|
||||
markdown: "Service: ~~<no value>~~",
|
||||
expected: "<p>Service: <del><no value></del></p>\n",
|
||||
},
|
||||
{
|
||||
name: "real html still omitted",
|
||||
markdown: "hello <div>world</div>",
|
||||
expected: "<p>hello <!-- raw HTML omitted -->world<!-- raw HTML omitted --></p>\n",
|
||||
},
|
||||
{
|
||||
name: "inside heading",
|
||||
markdown: "# Title <no value>",
|
||||
expected: "<h1>Title <no value></h1>\n",
|
||||
},
|
||||
{
|
||||
name: "inside list item",
|
||||
markdown: "- item <no value>",
|
||||
expected: "<ul>\n<li>item <no value></li>\n</ul>\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gm := goldmark.New(goldmark.WithExtensions(EscapeNoValue, extension.Strikethrough))
|
||||
var buf bytes.Buffer
|
||||
if err := gm.Convert([]byte(tt.markdown), &buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if buf.String() != tt.expected {
|
||||
t.Errorf("expected:\n%s\ngot:\n%s", tt.expected, buf.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -29,12 +29,13 @@ type AgentReport struct {
|
||||
}
|
||||
|
||||
type AccountConfig struct {
|
||||
// required till new providers are added
|
||||
AWS *AWSAccountConfig `json:"aws" required:"true" nullable:"false"`
|
||||
AWS *AWSAccountConfig `json:"aws" required:"false" nullable:"false"`
|
||||
Azure *AzureAccountConfig `json:"azure" required:"false" nullable:"false"`
|
||||
}
|
||||
|
||||
type AWSAccountConfig struct {
|
||||
Regions []string `json:"regions" required:"true" nullable:"false"`
|
||||
type UpdatableAccountConfig struct {
|
||||
AWS *UpdatableAWSAccountConfig `json:"aws" required:"false" nullable:"false"`
|
||||
Azure *UpdatableAzureAccountConfig `json:"azure" required:"false" nullable:"false"`
|
||||
}
|
||||
|
||||
type PostableAccount struct {
|
||||
@@ -45,7 +46,8 @@ type PostableAccount struct {
|
||||
type PostableAccountConfig struct {
|
||||
// as agent version is common for all providers, we can keep it at top level of this struct
|
||||
AgentVersion string
|
||||
Aws *AWSPostableAccountConfig `json:"aws" required:"true" nullable:"false"`
|
||||
AWS *AWSPostableAccountConfig `json:"aws" required:"false" nullable:"false"`
|
||||
Azure *AzurePostableAccountConfig `json:"azure" required:"false" nullable:"false"`
|
||||
}
|
||||
|
||||
type Credentials struct {
|
||||
@@ -55,11 +57,6 @@ type Credentials struct {
|
||||
IngestionKey string `json:"ingestionKey" required:"true"`
|
||||
}
|
||||
|
||||
type AWSPostableAccountConfig struct {
|
||||
DeploymentRegion string `json:"deploymentRegion" required:"true"`
|
||||
Regions []string `json:"regions" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type GettableAccountWithConnectionArtifact struct {
|
||||
ID valuer.UUID `json:"id" required:"true"`
|
||||
ConnectionArtifact *ConnectionArtifact `json:"connectionArtifact" required:"true"`
|
||||
@@ -67,11 +64,8 @@ type GettableAccountWithConnectionArtifact struct {
|
||||
|
||||
type ConnectionArtifact struct {
|
||||
// required till new providers are added
|
||||
Aws *AWSConnectionArtifact `json:"aws" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type AWSConnectionArtifact struct {
|
||||
ConnectionURL string `json:"connectionUrl" required:"true"`
|
||||
AWS *AWSConnectionArtifact `json:"aws" required:"false" nullable:"false"`
|
||||
Azure *AzureConnectionArtifact `json:"azure" required:"false" nullable:"false"`
|
||||
}
|
||||
|
||||
type GetConnectionArtifactRequest = PostableAccount
|
||||
@@ -81,7 +75,7 @@ type GettableAccounts struct {
|
||||
}
|
||||
|
||||
type UpdatableAccount struct {
|
||||
Config *AccountConfig `json:"config" required:"true" nullable:"false"`
|
||||
Config *UpdatableAccountConfig `json:"config" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
func NewAccount(orgID valuer.UUID, provider CloudProviderType, config *AccountConfig) *Account {
|
||||
@@ -173,25 +167,25 @@ func NewGettableAccounts(accounts []*Account) *GettableAccounts {
|
||||
func NewAccountConfigFromPostable(provider CloudProviderType, config *PostableAccountConfig) (*AccountConfig, error) {
|
||||
switch provider {
|
||||
case CloudProviderTypeAWS:
|
||||
if config.Aws == nil {
|
||||
if config.AWS == nil {
|
||||
return nil, errors.NewInvalidInputf(ErrCodeInvalidInput, "AWS config can not be nil for AWS provider")
|
||||
}
|
||||
|
||||
if err := validateAWSRegion(config.Aws.DeploymentRegion); err != nil {
|
||||
if err := validateAWSRegion(config.AWS.DeploymentRegion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(config.Aws.Regions) == 0 {
|
||||
if len(config.AWS.Regions) == 0 {
|
||||
return nil, errors.NewInvalidInputf(ErrCodeInvalidInput, "at least one region is required")
|
||||
}
|
||||
|
||||
for _, region := range config.Aws.Regions {
|
||||
for _, region := range config.AWS.Regions {
|
||||
if err := validateAWSRegion(region); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &AccountConfig{AWS: &AWSAccountConfig{Regions: config.Aws.Regions}}, nil
|
||||
return &AccountConfig{AWS: &AWSAccountConfig{Regions: config.AWS.Regions}}, nil
|
||||
default:
|
||||
return nil, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
|
||||
}
|
||||
@@ -303,3 +297,7 @@ func (config *AccountConfig) ToJSON() ([]byte, error) {
|
||||
|
||||
return nil, errors.NewInternalf(errors.CodeInternal, "no provider account config found")
|
||||
}
|
||||
|
||||
func NewIngestionKeyName(provider CloudProviderType) string {
|
||||
return fmt.Sprintf("%s-integration", provider.StringValue())
|
||||
}
|
||||
|
||||
@@ -51,11 +51,6 @@ type ProviderIntegrationConfig struct {
|
||||
AWS *AWSIntegrationConfig `json:"aws" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type AWSIntegrationConfig struct {
|
||||
EnabledRegions []string `json:"enabledRegions" required:"true" nullable:"false"`
|
||||
TelemetryCollectionStrategy *AWSTelemetryCollectionStrategy `json:"telemetryCollectionStrategy" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
// NewGettableAgentCheckIn constructs a backward-compatible response from an AgentCheckInResponse.
|
||||
// It populates the old snake_case fields (account_id, cloud_account_id, integration_config, removed_at)
|
||||
// from the new camelCase fields so older agents continue to work unchanged.
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package cloudintegrationtypes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// CloudProviderType type alias.
|
||||
type CloudProviderType struct{ valuer.String }
|
||||
|
||||
var (
|
||||
@@ -15,16 +12,9 @@ var (
|
||||
CloudProviderTypeAWS = CloudProviderType{valuer.NewString("aws")}
|
||||
CloudProviderTypeAzure = CloudProviderType{valuer.NewString("azure")}
|
||||
|
||||
// errors.
|
||||
ErrCodeCloudProviderInvalidInput = errors.MustNewCode("cloud_integration_invalid_cloud_provider")
|
||||
|
||||
CloudFormationQuickCreateBaseURL = valuer.NewString("https://%s.console.aws.amazon.com/cloudformation/home")
|
||||
AgentCloudFormationTemplateS3Path = valuer.NewString("https://signoz-integrations.s3.us-east-1.amazonaws.com/aws-quickcreate-template-%s.json")
|
||||
AgentCloudFormationBaseStackName = valuer.NewString("signoz-integration")
|
||||
)
|
||||
|
||||
// NewCloudProvider returns a new CloudProviderType from a string.
|
||||
// It validates the input and returns an error if the input is not valid cloud provider.
|
||||
func NewCloudProvider(provider string) (CloudProviderType, error) {
|
||||
switch provider {
|
||||
case CloudProviderTypeAWS.StringValue():
|
||||
@@ -35,7 +25,3 @@ func NewCloudProvider(provider string) (CloudProviderType, error) {
|
||||
return CloudProviderType{}, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider)
|
||||
}
|
||||
}
|
||||
|
||||
func NewIngestionKeyName(provider CloudProviderType) string {
|
||||
return fmt.Sprintf("%s-integration", provider.StringValue())
|
||||
}
|
||||
|
||||
169
pkg/types/cloudintegrationtypes/cloudprovider_aws.go
Normal file
169
pkg/types/cloudintegrationtypes/cloudprovider_aws.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package cloudintegrationtypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
var (
|
||||
CloudFormationQuickCreateBaseURL = valuer.NewString("https://%s.console.aws.amazon.com/cloudformation/home")
|
||||
AgentCloudFormationTemplateS3Path = valuer.NewString("https://signoz-integrations.s3.us-east-1.amazonaws.com/aws-quickcreate-template-%s.json")
|
||||
AgentCloudFormationBaseStackName = valuer.NewString("signoz-integration")
|
||||
)
|
||||
|
||||
type AWSPostableAccountConfig struct {
|
||||
DeploymentRegion string `json:"deploymentRegion" required:"true"`
|
||||
Regions []string `json:"regions" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type AWSConnectionArtifact struct {
|
||||
ConnectionURL string `json:"connectionUrl" required:"true"`
|
||||
}
|
||||
|
||||
type AWSAccountConfig struct {
|
||||
Regions []string `json:"regions" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type UpdatableAWSAccountConfig = AWSAccountConfig
|
||||
|
||||
// OldAWSCollectionStrategy is the backward-compatible snake_case form of AWSCollectionStrategy,
|
||||
// used in the legacy integration_config response field for older agents.
|
||||
type OldAWSCollectionStrategy struct {
|
||||
Provider string `json:"provider"`
|
||||
Metrics *OldAWSMetricsStrategy `json:"aws_metrics,omitempty"`
|
||||
Logs *OldAWSLogsStrategy `json:"aws_logs,omitempty"`
|
||||
S3Buckets map[string][]string `json:"s3_buckets,omitempty"`
|
||||
}
|
||||
|
||||
// OldAWSMetricsStrategy is the snake_case form of AWSMetricsStrategy for older agents.
|
||||
type OldAWSMetricsStrategy struct {
|
||||
StreamFilters []struct {
|
||||
Namespace string `json:"Namespace"`
|
||||
MetricNames []string `json:"MetricNames,omitempty"`
|
||||
} `json:"cloudwatch_metric_stream_filters"`
|
||||
}
|
||||
|
||||
// OldAWSLogsStrategy is the snake_case form of AWSLogsStrategy for older agents.
|
||||
type OldAWSLogsStrategy struct {
|
||||
Subscriptions []struct {
|
||||
LogGroupNamePrefix string `json:"log_group_name_prefix"`
|
||||
FilterPattern string `json:"filter_pattern"`
|
||||
} `json:"cloudwatch_logs_subscriptions"`
|
||||
}
|
||||
|
||||
type AWSIntegrationConfig struct {
|
||||
EnabledRegions []string `json:"enabledRegions" required:"true" nullable:"false"`
|
||||
TelemetryCollectionStrategy *AWSTelemetryCollectionStrategy `json:"telemetryCollectionStrategy" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
// AWSTelemetryCollectionStrategy represents signal collection strategy for AWS services.
|
||||
type AWSTelemetryCollectionStrategy struct {
|
||||
Metrics *AWSMetricsCollectionStrategy `json:"metrics,omitempty" required:"false" nullable:"false"`
|
||||
Logs *AWSLogsCollectionStrategy `json:"logs,omitempty" required:"false" nullable:"false"`
|
||||
S3Buckets map[string][]string `json:"s3Buckets,omitempty" required:"false"` // Only available in S3 Sync Service Type in AWS
|
||||
}
|
||||
|
||||
// AWSMetricsCollectionStrategy represents metrics collection strategy for AWS services.
|
||||
type AWSMetricsCollectionStrategy struct {
|
||||
// to be used as https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-metricstream.html#cfn-cloudwatch-metricstream-includefilters
|
||||
StreamFilters []*AWSCloudWatchMetricStreamFilter `json:"streamFilters" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type AWSCloudWatchMetricStreamFilter struct {
|
||||
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-metricstream-metricstreamfilter.html
|
||||
Namespace string `json:"namespace" required:"true"`
|
||||
MetricNames []string `json:"metricNames,omitempty" required:"false" nullable:"false"`
|
||||
}
|
||||
|
||||
// AWSLogsCollectionStrategy represents logs collection strategy for AWS services.
|
||||
type AWSLogsCollectionStrategy struct {
|
||||
Subscriptions []*AWSCloudWatchLogsSubscription `json:"subscriptions" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type AWSCloudWatchLogsSubscription struct {
|
||||
// subscribe to all logs groups with specified prefix.
|
||||
// eg: `/aws/rds/`
|
||||
LogGroupNamePrefix string `json:"logGroupNamePrefix" required:"true"`
|
||||
|
||||
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html
|
||||
// "" implies no filtering is required
|
||||
FilterPattern string `json:"filterPattern" required:"true"`
|
||||
}
|
||||
|
||||
type AWSServiceConfig struct {
|
||||
Logs *AWSServiceLogsConfig `json:"logs"`
|
||||
Metrics *AWSServiceMetricsConfig `json:"metrics"`
|
||||
}
|
||||
|
||||
// AWSServiceLogsConfig is AWS specific logs config for a service
|
||||
// NOTE: the JSON keys are snake case for backward compatibility with existing agents.
|
||||
type AWSServiceLogsConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
S3Buckets map[string][]string `json:"s3Buckets,omitempty"`
|
||||
}
|
||||
|
||||
type AWSServiceMetricsConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func NewAWSConnectionArtifact(connectionURL string) *AWSConnectionArtifact {
|
||||
return &AWSConnectionArtifact{
|
||||
ConnectionURL: connectionURL,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAWSIntegrationConfig(enabledRegions []string, telemetryCollectionStrategy *AWSTelemetryCollectionStrategy) *AWSIntegrationConfig {
|
||||
return &AWSIntegrationConfig{
|
||||
EnabledRegions: enabledRegions,
|
||||
TelemetryCollectionStrategy: telemetryCollectionStrategy,
|
||||
}
|
||||
}
|
||||
|
||||
// awsOlderIntegrationConfig converts a ProviderIntegrationConfig into the legacy snake_case
|
||||
// IntegrationConfig format consumed by older AWS agents. Returns nil if AWS config is absent.
|
||||
func awsOlderIntegrationConfig(cfg *ProviderIntegrationConfig) *IntegrationConfig {
|
||||
if cfg == nil || cfg.AWS == nil {
|
||||
return nil
|
||||
}
|
||||
awsCfg := cfg.AWS
|
||||
|
||||
older := &IntegrationConfig{
|
||||
EnabledRegions: awsCfg.EnabledRegions,
|
||||
}
|
||||
|
||||
if awsCfg.TelemetryCollectionStrategy == nil {
|
||||
return older
|
||||
}
|
||||
|
||||
// Older agents expect a "provider" field and fully snake_case keys inside telemetry.
|
||||
oldTelemetry := &OldAWSCollectionStrategy{
|
||||
Provider: CloudProviderTypeAWS.StringValue(),
|
||||
S3Buckets: awsCfg.TelemetryCollectionStrategy.S3Buckets,
|
||||
}
|
||||
|
||||
if awsCfg.TelemetryCollectionStrategy.Metrics != nil {
|
||||
// Convert camelCase cloudwatchMetricStreamFilters → snake_case cloudwatch_metric_stream_filters
|
||||
oldMetrics := &OldAWSMetricsStrategy{}
|
||||
for _, f := range awsCfg.TelemetryCollectionStrategy.Metrics.StreamFilters {
|
||||
oldMetrics.StreamFilters = append(oldMetrics.StreamFilters, struct {
|
||||
Namespace string `json:"Namespace"`
|
||||
MetricNames []string `json:"MetricNames,omitempty"`
|
||||
}{Namespace: f.Namespace, MetricNames: f.MetricNames})
|
||||
}
|
||||
oldTelemetry.Metrics = oldMetrics
|
||||
}
|
||||
|
||||
if awsCfg.TelemetryCollectionStrategy.Logs != nil {
|
||||
// Convert camelCase cloudwatchLogsSubscriptions → snake_case cloudwatch_logs_subscriptions
|
||||
oldLogs := &OldAWSLogsStrategy{}
|
||||
for _, s := range awsCfg.TelemetryCollectionStrategy.Logs.Subscriptions {
|
||||
oldLogs.Subscriptions = append(oldLogs.Subscriptions, struct {
|
||||
LogGroupNamePrefix string `json:"log_group_name_prefix"`
|
||||
FilterPattern string `json:"filter_pattern"`
|
||||
}{LogGroupNamePrefix: s.LogGroupNamePrefix, FilterPattern: s.FilterPattern})
|
||||
}
|
||||
oldTelemetry.Logs = oldLogs
|
||||
}
|
||||
|
||||
older.Telemetry = oldTelemetry
|
||||
return older
|
||||
}
|
||||
45
pkg/types/cloudintegrationtypes/cloudprovider_azure.go
Normal file
45
pkg/types/cloudintegrationtypes/cloudprovider_azure.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package cloudintegrationtypes
|
||||
|
||||
type AzureAccountConfig struct {
|
||||
DeploymentRegion string `json:"deploymentRegion" required:"true"`
|
||||
ResourceGroups []string `json:"resourceGroups" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type UpdatableAzureAccountConfig struct {
|
||||
ResourceGroups []string `json:"resourceGroups" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type AzurePostableAccountConfig = AzureAccountConfig
|
||||
|
||||
type AzureConnectionArtifact struct {
|
||||
CLICommand string `json:"cliCommand" required:"true"`
|
||||
CloudShellCommand string `json:"cloudShellCommand" required:"true"`
|
||||
}
|
||||
|
||||
type AzureServiceConfig struct {
|
||||
Logs *AzureServiceLogsConfig `json:"logs,omitempty"`
|
||||
Metrics *AzureServiceMetricsConfig `json:"metrics,omitempty"`
|
||||
}
|
||||
|
||||
type AzureServiceLogsConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type AzureServiceMetricsConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type AzureTelemetryCollectionStrategy struct {
|
||||
Metrics *AzureMetricsCollectionStrategy `json:"metrics,omitempty" required:"false" nullable:"false"`
|
||||
Logs *AzureLogsCollectionStrategy `json:"logs,omitempty" required:"false" nullable:"false"`
|
||||
}
|
||||
|
||||
type AzureMetricsCollectionStrategy struct {
|
||||
// Azure service namespaces to collect metrics from, e.g., "Microsoft.EventHub/namespaces"
|
||||
ServiceNames []string `json:"serviceNames" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type AzureLogsCollectionStrategy struct {
|
||||
// List of categories to enable for diagnostic settings, to start with it will have 'allLogs' and no filtering.
|
||||
Categories []string `json:"categories,omitempty" required:"false" nullable:"false"`
|
||||
}
|
||||
@@ -2,107 +2,161 @@ package cloudintegrationtypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
var ErrCodeInvalidCloudRegion = errors.MustNewCode("invalid_cloud_region")
|
||||
type CloudProviderRegion struct{ valuer.String }
|
||||
|
||||
// List of all valid cloud regions on Amazon Web Services.
|
||||
var ValidAWSRegions = map[string]struct{}{
|
||||
"af-south-1": {}, // Africa (Cape Town).
|
||||
"ap-east-1": {}, // Asia Pacific (Hong Kong).
|
||||
"ap-northeast-1": {}, // Asia Pacific (Tokyo).
|
||||
"ap-northeast-2": {}, // Asia Pacific (Seoul).
|
||||
"ap-northeast-3": {}, // Asia Pacific (Osaka).
|
||||
"ap-south-1": {}, // Asia Pacific (Mumbai).
|
||||
"ap-south-2": {}, // Asia Pacific (Hyderabad).
|
||||
"ap-southeast-1": {}, // Asia Pacific (Singapore).
|
||||
"ap-southeast-2": {}, // Asia Pacific (Sydney).
|
||||
"ap-southeast-3": {}, // Asia Pacific (Jakarta).
|
||||
"ap-southeast-4": {}, // Asia Pacific (Melbourne).
|
||||
"ca-central-1": {}, // Canada (Central).
|
||||
"ca-west-1": {}, // Canada West (Calgary).
|
||||
"eu-central-1": {}, // Europe (Frankfurt).
|
||||
"eu-central-2": {}, // Europe (Zurich).
|
||||
"eu-north-1": {}, // Europe (Stockholm).
|
||||
"eu-south-1": {}, // Europe (Milan).
|
||||
"eu-south-2": {}, // Europe (Spain).
|
||||
"eu-west-1": {}, // Europe (Ireland).
|
||||
"eu-west-2": {}, // Europe (London).
|
||||
"eu-west-3": {}, // Europe (Paris).
|
||||
"il-central-1": {}, // Israel (Tel Aviv).
|
||||
"me-central-1": {}, // Middle East (UAE).
|
||||
"me-south-1": {}, // Middle East (Bahrain).
|
||||
"sa-east-1": {}, // South America (Sao Paulo).
|
||||
"us-east-1": {}, // US East (N. Virginia).
|
||||
"us-east-2": {}, // US East (Ohio).
|
||||
"us-west-1": {}, // US West (N. California).
|
||||
"us-west-2": {}, // US West (Oregon).
|
||||
var ErrCodeInvalidCloudRegion = errors.MustNewCode("invalid_cloud_region")
|
||||
var (
|
||||
// AWS regions.
|
||||
AWSRegionAFSouth1 = CloudProviderRegion{valuer.NewString("af-south-1")} // Africa (Cape Town).
|
||||
AWSRegionAPEast1 = CloudProviderRegion{valuer.NewString("ap-east-1")} // Asia Pacific (Hong Kong).
|
||||
AWSRegionAPNortheast1 = CloudProviderRegion{valuer.NewString("ap-northeast-1")} // Asia Pacific (Tokyo).
|
||||
AWSRegionAPNortheast2 = CloudProviderRegion{valuer.NewString("ap-northeast-2")} // Asia Pacific (Seoul).
|
||||
AWSRegionAPNortheast3 = CloudProviderRegion{valuer.NewString("ap-northeast-3")} // Asia Pacific (Osaka).
|
||||
AWSRegionAPSouth1 = CloudProviderRegion{valuer.NewString("ap-south-1")} // Asia Pacific (Mumbai).
|
||||
AWSRegionAPSouth2 = CloudProviderRegion{valuer.NewString("ap-south-2")} // Asia Pacific (Hyderabad).
|
||||
AWSRegionAPSoutheast1 = CloudProviderRegion{valuer.NewString("ap-southeast-1")} // Asia Pacific (Singapore).
|
||||
AWSRegionAPSoutheast2 = CloudProviderRegion{valuer.NewString("ap-southeast-2")} // Asia Pacific (Sydney).
|
||||
AWSRegionAPSoutheast3 = CloudProviderRegion{valuer.NewString("ap-southeast-3")} // Asia Pacific (Jakarta).
|
||||
AWSRegionAPSoutheast4 = CloudProviderRegion{valuer.NewString("ap-southeast-4")} // Asia Pacific (Melbourne).
|
||||
AWSRegionCACentral1 = CloudProviderRegion{valuer.NewString("ca-central-1")} // Canada (Central).
|
||||
AWSRegionCAWest1 = CloudProviderRegion{valuer.NewString("ca-west-1")} // Canada West (Calgary).
|
||||
AWSRegionEUCentral1 = CloudProviderRegion{valuer.NewString("eu-central-1")} // Europe (Frankfurt).
|
||||
AWSRegionEUCentral2 = CloudProviderRegion{valuer.NewString("eu-central-2")} // Europe (Zurich).
|
||||
AWSRegionEUNorth1 = CloudProviderRegion{valuer.NewString("eu-north-1")} // Europe (Stockholm).
|
||||
AWSRegionEUSouth1 = CloudProviderRegion{valuer.NewString("eu-south-1")} // Europe (Milan).
|
||||
AWSRegionEUSouth2 = CloudProviderRegion{valuer.NewString("eu-south-2")} // Europe (Spain).
|
||||
AWSRegionEUWest1 = CloudProviderRegion{valuer.NewString("eu-west-1")} // Europe (Ireland).
|
||||
AWSRegionEUWest2 = CloudProviderRegion{valuer.NewString("eu-west-2")} // Europe (London).
|
||||
AWSRegionEUWest3 = CloudProviderRegion{valuer.NewString("eu-west-3")} // Europe (Paris).
|
||||
AWSRegionILCentral1 = CloudProviderRegion{valuer.NewString("il-central-1")} // Israel (Tel Aviv).
|
||||
AWSRegionMECentral1 = CloudProviderRegion{valuer.NewString("me-central-1")} // Middle East (UAE).
|
||||
AWSRegionMESouth1 = CloudProviderRegion{valuer.NewString("me-south-1")} // Middle East (Bahrain).
|
||||
AWSRegionSAEast1 = CloudProviderRegion{valuer.NewString("sa-east-1")} // South America (Sao Paulo).
|
||||
AWSRegionUSEast1 = CloudProviderRegion{valuer.NewString("us-east-1")} // US East (N. Virginia).
|
||||
AWSRegionUSEast2 = CloudProviderRegion{valuer.NewString("us-east-2")} // US East (Ohio).
|
||||
AWSRegionUSWest1 = CloudProviderRegion{valuer.NewString("us-west-1")} // US West (N. California).
|
||||
AWSRegionUSWest2 = CloudProviderRegion{valuer.NewString("us-west-2")} // US West (Oregon).
|
||||
|
||||
// Azure regions.
|
||||
AzureRegionAustraliaCentral = CloudProviderRegion{valuer.NewString("australiacentral")} // Australia Central.
|
||||
AzureRegionAustraliaCentral2 = CloudProviderRegion{valuer.NewString("australiacentral2")} // Australia Central 2.
|
||||
AzureRegionAustraliaEast = CloudProviderRegion{valuer.NewString("australiaeast")} // Australia East.
|
||||
AzureRegionAustraliaSoutheast = CloudProviderRegion{valuer.NewString("australiasoutheast")} // Australia Southeast.
|
||||
AzureRegionAustriaEast = CloudProviderRegion{valuer.NewString("austriaeast")} // Austria East.
|
||||
AzureRegionBelgiumCentral = CloudProviderRegion{valuer.NewString("belgiumcentral")} // Belgium Central.
|
||||
AzureRegionBrazilSouth = CloudProviderRegion{valuer.NewString("brazilsouth")} // Brazil South.
|
||||
AzureRegionBrazilSoutheast = CloudProviderRegion{valuer.NewString("brazilsoutheast")} // Brazil Southeast.
|
||||
AzureRegionCanadaCentral = CloudProviderRegion{valuer.NewString("canadacentral")} // Canada Central.
|
||||
AzureRegionCanadaEast = CloudProviderRegion{valuer.NewString("canadaeast")} // Canada East.
|
||||
AzureRegionCentralIndia = CloudProviderRegion{valuer.NewString("centralindia")} // Central India.
|
||||
AzureRegionCentralUS = CloudProviderRegion{valuer.NewString("centralus")} // Central US.
|
||||
AzureRegionChileCentral = CloudProviderRegion{valuer.NewString("chilecentral")} // Chile Central.
|
||||
AzureRegionDenmarkEast = CloudProviderRegion{valuer.NewString("denmarkeast")} // Denmark East.
|
||||
AzureRegionEastAsia = CloudProviderRegion{valuer.NewString("eastasia")} // East Asia.
|
||||
AzureRegionEastUS = CloudProviderRegion{valuer.NewString("eastus")} // East US.
|
||||
AzureRegionEastUS2 = CloudProviderRegion{valuer.NewString("eastus2")} // East US 2.
|
||||
AzureRegionFranceCentral = CloudProviderRegion{valuer.NewString("francecentral")} // France Central.
|
||||
AzureRegionFranceSouth = CloudProviderRegion{valuer.NewString("francesouth")} // France South.
|
||||
AzureRegionGermanyNorth = CloudProviderRegion{valuer.NewString("germanynorth")} // Germany North.
|
||||
AzureRegionGermanyWestCentral = CloudProviderRegion{valuer.NewString("germanywestcentral")} // Germany West Central.
|
||||
AzureRegionIndonesiaCentral = CloudProviderRegion{valuer.NewString("indonesiacentral")} // Indonesia Central.
|
||||
AzureRegionIsraelCentral = CloudProviderRegion{valuer.NewString("israelcentral")} // Israel Central.
|
||||
AzureRegionItalyNorth = CloudProviderRegion{valuer.NewString("italynorth")} // Italy North.
|
||||
AzureRegionJapanEast = CloudProviderRegion{valuer.NewString("japaneast")} // Japan East.
|
||||
AzureRegionJapanWest = CloudProviderRegion{valuer.NewString("japanwest")} // Japan West.
|
||||
AzureRegionKoreaCentral = CloudProviderRegion{valuer.NewString("koreacentral")} // Korea Central.
|
||||
AzureRegionKoreaSouth = CloudProviderRegion{valuer.NewString("koreasouth")} // Korea South.
|
||||
AzureRegionMalaysiaWest = CloudProviderRegion{valuer.NewString("malaysiawest")} // Malaysia West.
|
||||
AzureRegionMexicoCentral = CloudProviderRegion{valuer.NewString("mexicocentral")} // Mexico Central.
|
||||
AzureRegionNewZealandNorth = CloudProviderRegion{valuer.NewString("newzealandnorth")} // New Zealand North.
|
||||
AzureRegionNorthCentralUS = CloudProviderRegion{valuer.NewString("northcentralus")} // North Central US.
|
||||
AzureRegionNorthEurope = CloudProviderRegion{valuer.NewString("northeurope")} // North Europe.
|
||||
AzureRegionNorwayEast = CloudProviderRegion{valuer.NewString("norwayeast")} // Norway East.
|
||||
AzureRegionNorwayWest = CloudProviderRegion{valuer.NewString("norwaywest")} // Norway West.
|
||||
AzureRegionPolandCentral = CloudProviderRegion{valuer.NewString("polandcentral")} // Poland Central.
|
||||
AzureRegionQatarCentral = CloudProviderRegion{valuer.NewString("qatarcentral")} // Qatar Central.
|
||||
AzureRegionSouthAfricaNorth = CloudProviderRegion{valuer.NewString("southafricanorth")} // South Africa North.
|
||||
AzureRegionSouthAfricaWest = CloudProviderRegion{valuer.NewString("southafricawest")} // South Africa West.
|
||||
AzureRegionSouthCentralUS = CloudProviderRegion{valuer.NewString("southcentralus")} // South Central US.
|
||||
AzureRegionSouthIndia = CloudProviderRegion{valuer.NewString("southindia")} // South India.
|
||||
AzureRegionSoutheastAsia = CloudProviderRegion{valuer.NewString("southeastasia")} // Southeast Asia.
|
||||
AzureRegionSpainCentral = CloudProviderRegion{valuer.NewString("spaincentral")} // Spain Central.
|
||||
AzureRegionSwedenCentral = CloudProviderRegion{valuer.NewString("swedencentral")} // Sweden Central.
|
||||
AzureRegionSwitzerlandNorth = CloudProviderRegion{valuer.NewString("switzerlandnorth")} // Switzerland North.
|
||||
AzureRegionSwitzerlandWest = CloudProviderRegion{valuer.NewString("switzerlandwest")} // Switzerland West.
|
||||
AzureRegionUAECentral = CloudProviderRegion{valuer.NewString("uaecentral")} // UAE Central.
|
||||
AzureRegionUAENorth = CloudProviderRegion{valuer.NewString("uaenorth")} // UAE North.
|
||||
AzureRegionUKSouth = CloudProviderRegion{valuer.NewString("uksouth")} // UK South.
|
||||
AzureRegionUKWest = CloudProviderRegion{valuer.NewString("ukwest")} // UK West.
|
||||
AzureRegionWestCentralUS = CloudProviderRegion{valuer.NewString("westcentralus")} // West Central US.
|
||||
AzureRegionWestEurope = CloudProviderRegion{valuer.NewString("westeurope")} // West Europe.
|
||||
AzureRegionWestIndia = CloudProviderRegion{valuer.NewString("westindia")} // West India.
|
||||
AzureRegionWestUS = CloudProviderRegion{valuer.NewString("westus")} // West US.
|
||||
AzureRegionWestUS2 = CloudProviderRegion{valuer.NewString("westus2")} // West US 2.
|
||||
AzureRegionWestUS3 = CloudProviderRegion{valuer.NewString("westus3")} // West US 3.
|
||||
)
|
||||
|
||||
func Enum() []any {
|
||||
return []any{
|
||||
// AWS regions.
|
||||
AWSRegionAFSouth1, AWSRegionAPEast1, AWSRegionAPNortheast1, AWSRegionAPNortheast2, AWSRegionAPNortheast3,
|
||||
AWSRegionAPSouth1, AWSRegionAPSouth2, AWSRegionAPSoutheast1, AWSRegionAPSoutheast2, AWSRegionAPSoutheast3,
|
||||
AWSRegionAPSoutheast4, AWSRegionCACentral1, AWSRegionCAWest1, AWSRegionEUCentral1, AWSRegionEUCentral2, AWSRegionEUNorth1,
|
||||
AWSRegionEUSouth1, AWSRegionEUSouth2, AWSRegionEUWest1, AWSRegionEUWest2, AWSRegionEUWest3,
|
||||
AWSRegionILCentral1, AWSRegionMECentral1, AWSRegionMESouth1, AWSRegionSAEast1, AWSRegionUSEast1, AWSRegionUSEast2,
|
||||
AWSRegionUSWest1, AWSRegionUSWest2,
|
||||
// Azure regions.
|
||||
AzureRegionAustraliaCentral, AzureRegionAustraliaCentral2, AzureRegionAustraliaEast, AzureRegionAustraliaSoutheast,
|
||||
AzureRegionAustriaEast, AzureRegionBelgiumCentral, AzureRegionBrazilSouth, AzureRegionBrazilSoutheast,
|
||||
AzureRegionCanadaCentral, AzureRegionCanadaEast, AzureRegionCentralIndia, AzureRegionCentralUS,
|
||||
AzureRegionChileCentral, AzureRegionDenmarkEast, AzureRegionEastAsia, AzureRegionEastUS, AzureRegionEastUS2,
|
||||
AzureRegionFranceCentral, AzureRegionFranceSouth, AzureRegionGermanyNorth, AzureRegionGermanyWestCentral,
|
||||
AzureRegionIndonesiaCentral, AzureRegionIsraelCentral, AzureRegionItalyNorth, AzureRegionJapanEast, AzureRegionJapanWest,
|
||||
AzureRegionKoreaCentral, AzureRegionKoreaSouth, AzureRegionMalaysiaWest, AzureRegionMexicoCentral,
|
||||
AzureRegionNewZealandNorth, AzureRegionNorthCentralUS, AzureRegionNorthEurope, AzureRegionNorwayEast, AzureRegionNorwayWest,
|
||||
AzureRegionPolandCentral, AzureRegionQatarCentral, AzureRegionSouthAfricaNorth, AzureRegionSouthAfricaWest,
|
||||
AzureRegionSouthCentralUS, AzureRegionSouthIndia, AzureRegionSoutheastAsia, AzureRegionSpainCentral,
|
||||
AzureRegionSwedenCentral, AzureRegionSwitzerlandNorth, AzureRegionSwitzerlandWest,
|
||||
AzureRegionUAECentral, AzureRegionUAENorth, AzureRegionUKSouth, AzureRegionUKWest,
|
||||
AzureRegionWestCentralUS, AzureRegionWestEurope, AzureRegionWestIndia, AzureRegionWestUS, AzureRegionWestUS2, AzureRegionWestUS3,
|
||||
}
|
||||
}
|
||||
|
||||
// List of all valid cloud regions for Microsoft Azure.
|
||||
var ValidAzureRegions = map[string]struct{}{
|
||||
"australiacentral": {}, // Australia Central
|
||||
"australiacentral2": {}, // Australia Central 2
|
||||
"australiaeast": {}, // Australia East
|
||||
"australiasoutheast": {}, // Australia Southeast
|
||||
"austriaeast": {}, // Austria East
|
||||
"belgiumcentral": {}, // Belgium Central
|
||||
"brazilsouth": {}, // Brazil South
|
||||
"brazilsoutheast": {}, // Brazil Southeast
|
||||
"canadacentral": {}, // Canada Central
|
||||
"canadaeast": {}, // Canada East
|
||||
"centralindia": {}, // Central India
|
||||
"centralus": {}, // Central US
|
||||
"chilecentral": {}, // Chile Central
|
||||
"denmarkeast": {}, // Denmark East
|
||||
"eastasia": {}, // East Asia
|
||||
"eastus": {}, // East US
|
||||
"eastus2": {}, // East US 2
|
||||
"francecentral": {}, // France Central
|
||||
"francesouth": {}, // France South
|
||||
"germanynorth": {}, // Germany North
|
||||
"germanywestcentral": {}, // Germany West Central
|
||||
"indonesiacentral": {}, // Indonesia Central
|
||||
"israelcentral": {}, // Israel Central
|
||||
"italynorth": {}, // Italy North
|
||||
"japaneast": {}, // Japan East
|
||||
"japanwest": {}, // Japan West
|
||||
"koreacentral": {}, // Korea Central
|
||||
"koreasouth": {}, // Korea South
|
||||
"malaysiawest": {}, // Malaysia West
|
||||
"mexicocentral": {}, // Mexico Central
|
||||
"newzealandnorth": {}, // New Zealand North
|
||||
"northcentralus": {}, // North Central US
|
||||
"northeurope": {}, // North Europe
|
||||
"norwayeast": {}, // Norway East
|
||||
"norwaywest": {}, // Norway West
|
||||
"polandcentral": {}, // Poland Central
|
||||
"qatarcentral": {}, // Qatar Central
|
||||
"southafricanorth": {}, // South Africa North
|
||||
"southafricawest": {}, // South Africa West
|
||||
"southcentralus": {}, // South Central US
|
||||
"southindia": {}, // South India
|
||||
"southeastasia": {}, // Southeast Asia
|
||||
"spaincentral": {}, // Spain Central
|
||||
"swedencentral": {}, // Sweden Central
|
||||
"switzerlandnorth": {}, // Switzerland North
|
||||
"switzerlandwest": {}, // Switzerland West
|
||||
"uaecentral": {}, // UAE Central
|
||||
"uaenorth": {}, // UAE North
|
||||
"uksouth": {}, // UK South
|
||||
"ukwest": {}, // UK West
|
||||
"westcentralus": {}, // West Central US
|
||||
"westeurope": {}, // West Europe
|
||||
"westindia": {}, // West India
|
||||
"westus": {}, // West US
|
||||
"westus2": {}, // West US 2
|
||||
"westus3": {}, // West US 3
|
||||
var SupportedRegions = map[CloudProviderType][]CloudProviderRegion{
|
||||
CloudProviderTypeAWS: {
|
||||
AWSRegionAFSouth1, AWSRegionAPEast1, AWSRegionAPNortheast1, AWSRegionAPNortheast2, AWSRegionAPNortheast3,
|
||||
AWSRegionAPSouth1, AWSRegionAPSouth2, AWSRegionAPSoutheast1, AWSRegionAPSoutheast2, AWSRegionAPSoutheast3,
|
||||
AWSRegionAPSoutheast4, AWSRegionCACentral1, AWSRegionCAWest1, AWSRegionEUCentral1, AWSRegionEUCentral2, AWSRegionEUNorth1,
|
||||
AWSRegionEUSouth1, AWSRegionEUSouth2, AWSRegionEUWest1, AWSRegionEUWest2, AWSRegionEUWest3,
|
||||
AWSRegionILCentral1, AWSRegionMECentral1, AWSRegionMESouth1, AWSRegionSAEast1, AWSRegionUSEast1, AWSRegionUSEast2,
|
||||
AWSRegionUSWest1, AWSRegionUSWest2,
|
||||
},
|
||||
CloudProviderTypeAzure: {
|
||||
AzureRegionAustraliaCentral, AzureRegionAustraliaCentral2, AzureRegionAustraliaEast, AzureRegionAustraliaSoutheast,
|
||||
AzureRegionAustriaEast, AzureRegionBelgiumCentral, AzureRegionBrazilSouth, AzureRegionBrazilSoutheast,
|
||||
AzureRegionCanadaCentral, AzureRegionCanadaEast, AzureRegionCentralIndia, AzureRegionCentralUS,
|
||||
AzureRegionChileCentral, AzureRegionDenmarkEast, AzureRegionEastAsia, AzureRegionEastUS, AzureRegionEastUS2,
|
||||
AzureRegionFranceCentral, AzureRegionFranceSouth, AzureRegionGermanyNorth, AzureRegionGermanyWestCentral,
|
||||
AzureRegionIndonesiaCentral, AzureRegionIsraelCentral, AzureRegionItalyNorth, AzureRegionJapanEast, AzureRegionJapanWest,
|
||||
AzureRegionKoreaCentral, AzureRegionKoreaSouth, AzureRegionMalaysiaWest, AzureRegionMexicoCentral,
|
||||
AzureRegionNewZealandNorth, AzureRegionNorthCentralUS, AzureRegionNorthEurope, AzureRegionNorwayEast, AzureRegionNorwayWest,
|
||||
AzureRegionPolandCentral, AzureRegionQatarCentral, AzureRegionSouthAfricaNorth, AzureRegionSouthAfricaWest,
|
||||
AzureRegionSouthCentralUS, AzureRegionSouthIndia, AzureRegionSoutheastAsia, AzureRegionSpainCentral,
|
||||
AzureRegionSwedenCentral, AzureRegionSwitzerlandNorth, AzureRegionSwitzerlandWest,
|
||||
AzureRegionUAECentral, AzureRegionUAENorth, AzureRegionUKSouth, AzureRegionUKWest,
|
||||
AzureRegionWestCentralUS, AzureRegionWestEurope, AzureRegionWestIndia, AzureRegionWestUS, AzureRegionWestUS2, AzureRegionWestUS3,
|
||||
},
|
||||
}
|
||||
|
||||
func validateAWSRegion(region string) error {
|
||||
_, ok := ValidAWSRegions[region]
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(ErrCodeInvalidCloudRegion, "invalid AWS region: %s", region)
|
||||
for _, r := range SupportedRegions[CloudProviderTypeAWS] {
|
||||
if r.StringValue() == region {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return errors.NewInvalidInputf(ErrCodeInvalidCloudRegion, "invalid AWS region: %s", region)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
var ErrCodeInvalidServiceID = errors.MustNewCode("invalid_service_id")
|
||||
|
||||
type CloudIntegrationService struct {
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
@@ -23,24 +21,8 @@ type CloudIntegrationService struct {
|
||||
}
|
||||
|
||||
type ServiceConfig struct {
|
||||
// required till new providers are added
|
||||
AWS *AWSServiceConfig `json:"aws" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type AWSServiceConfig struct {
|
||||
Logs *AWSServiceLogsConfig `json:"logs"`
|
||||
Metrics *AWSServiceMetricsConfig `json:"metrics"`
|
||||
}
|
||||
|
||||
// AWSServiceLogsConfig is AWS specific logs config for a service
|
||||
// NOTE: the JSON keys are snake case for backward compatibility with existing agents.
|
||||
type AWSServiceLogsConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
S3Buckets map[string][]string `json:"s3Buckets,omitempty"`
|
||||
}
|
||||
|
||||
type AWSServiceMetricsConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AWS *AWSServiceConfig `json:"aws" required:"false" nullable:"false"`
|
||||
Azure *AzureServiceConfig `json:"azure" required:"false" nullable:"false"`
|
||||
}
|
||||
|
||||
// ServiceMetadata helps to quickly list available services and whether it is enabled or not.
|
||||
@@ -106,7 +88,8 @@ type DataCollected struct {
|
||||
// TelemetryCollectionStrategy is cloud provider specific configuration for signal collection,
|
||||
// this is used by agent to understand the nitty-gritty for collecting telemetry for the cloud provider.
|
||||
type TelemetryCollectionStrategy struct {
|
||||
AWS *AWSTelemetryCollectionStrategy `json:"aws" required:"true" nullable:"false"`
|
||||
AWS *AWSTelemetryCollectionStrategy `json:"aws" required:"false" nullable:"false"`
|
||||
Azure *AzureTelemetryCollectionStrategy `json:"azure" required:"false" nullable:"false"`
|
||||
}
|
||||
|
||||
// Assets represents the collection of dashboards.
|
||||
@@ -130,65 +113,6 @@ type CollectedMetric struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// OldAWSCollectionStrategy is the backward-compatible snake_case form of AWSCollectionStrategy,
|
||||
// used in the legacy integration_config response field for older agents.
|
||||
type OldAWSCollectionStrategy struct {
|
||||
Provider string `json:"provider"`
|
||||
Metrics *OldAWSMetricsStrategy `json:"aws_metrics,omitempty"`
|
||||
Logs *OldAWSLogsStrategy `json:"aws_logs,omitempty"`
|
||||
S3Buckets map[string][]string `json:"s3_buckets,omitempty"`
|
||||
}
|
||||
|
||||
// OldAWSMetricsStrategy is the snake_case form of AWSMetricsStrategy for older agents.
|
||||
type OldAWSMetricsStrategy struct {
|
||||
StreamFilters []struct {
|
||||
Namespace string `json:"Namespace"`
|
||||
MetricNames []string `json:"MetricNames,omitempty"`
|
||||
} `json:"cloudwatch_metric_stream_filters"`
|
||||
}
|
||||
|
||||
// OldAWSLogsStrategy is the snake_case form of AWSLogsStrategy for older agents.
|
||||
type OldAWSLogsStrategy struct {
|
||||
Subscriptions []struct {
|
||||
LogGroupNamePrefix string `json:"log_group_name_prefix"`
|
||||
FilterPattern string `json:"filter_pattern"`
|
||||
} `json:"cloudwatch_logs_subscriptions"`
|
||||
}
|
||||
|
||||
// AWSTelemetryCollectionStrategy represents signal collection strategy for AWS services.
|
||||
type AWSTelemetryCollectionStrategy struct {
|
||||
Metrics *AWSMetricsCollectionStrategy `json:"metrics,omitempty" required:"false" nullable:"false"`
|
||||
Logs *AWSLogsCollectionStrategy `json:"logs,omitempty" required:"false" nullable:"false"`
|
||||
S3Buckets map[string][]string `json:"s3Buckets,omitempty" required:"false"` // Only available in S3 Sync Service Type in AWS
|
||||
}
|
||||
|
||||
// AWSMetricsCollectionStrategy represents metrics collection strategy for AWS services.
|
||||
type AWSMetricsCollectionStrategy struct {
|
||||
// to be used as https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-metricstream.html#cfn-cloudwatch-metricstream-includefilters
|
||||
StreamFilters []*AWSCloudWatchMetricStreamFilter `json:"streamFilters" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type AWSCloudWatchMetricStreamFilter struct {
|
||||
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-metricstream-metricstreamfilter.html
|
||||
Namespace string `json:"namespace" required:"true"`
|
||||
MetricNames []string `json:"metricNames,omitempty" required:"false" nullable:"false"`
|
||||
}
|
||||
|
||||
// AWSLogsCollectionStrategy represents logs collection strategy for AWS services.
|
||||
type AWSLogsCollectionStrategy struct {
|
||||
Subscriptions []*AWSCloudWatchLogsSubscription `json:"subscriptions" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type AWSCloudWatchLogsSubscription struct {
|
||||
// subscribe to all logs groups with specified prefix.
|
||||
// eg: `/aws/rds/`
|
||||
LogGroupNamePrefix string `json:"logGroupNamePrefix" required:"true"`
|
||||
|
||||
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html
|
||||
// "" implies no filtering is required
|
||||
FilterPattern string `json:"filterPattern" required:"true"`
|
||||
}
|
||||
|
||||
// Dashboard represents a dashboard definition for cloud integration.
|
||||
// This is used to show available pre-made dashboards for a service,
|
||||
// hence has additional fields like id, title and description.
|
||||
@@ -408,53 +332,3 @@ func GetDashboardsFromAssets(
|
||||
|
||||
return dashboards
|
||||
}
|
||||
|
||||
// awsOlderIntegrationConfig converts a ProviderIntegrationConfig into the legacy snake_case
|
||||
// IntegrationConfig format consumed by older AWS agents. Returns nil if AWS config is absent.
|
||||
func awsOlderIntegrationConfig(cfg *ProviderIntegrationConfig) *IntegrationConfig {
|
||||
if cfg == nil || cfg.AWS == nil {
|
||||
return nil
|
||||
}
|
||||
awsCfg := cfg.AWS
|
||||
|
||||
older := &IntegrationConfig{
|
||||
EnabledRegions: awsCfg.EnabledRegions,
|
||||
}
|
||||
|
||||
if awsCfg.TelemetryCollectionStrategy == nil {
|
||||
return older
|
||||
}
|
||||
|
||||
// Older agents expect a "provider" field and fully snake_case keys inside telemetry.
|
||||
oldTelemetry := &OldAWSCollectionStrategy{
|
||||
Provider: CloudProviderTypeAWS.StringValue(),
|
||||
S3Buckets: awsCfg.TelemetryCollectionStrategy.S3Buckets,
|
||||
}
|
||||
|
||||
if awsCfg.TelemetryCollectionStrategy.Metrics != nil {
|
||||
// Convert camelCase cloudwatchMetricStreamFilters → snake_case cloudwatch_metric_stream_filters
|
||||
oldMetrics := &OldAWSMetricsStrategy{}
|
||||
for _, f := range awsCfg.TelemetryCollectionStrategy.Metrics.StreamFilters {
|
||||
oldMetrics.StreamFilters = append(oldMetrics.StreamFilters, struct {
|
||||
Namespace string `json:"Namespace"`
|
||||
MetricNames []string `json:"MetricNames,omitempty"`
|
||||
}{Namespace: f.Namespace, MetricNames: f.MetricNames})
|
||||
}
|
||||
oldTelemetry.Metrics = oldMetrics
|
||||
}
|
||||
|
||||
if awsCfg.TelemetryCollectionStrategy.Logs != nil {
|
||||
// Convert camelCase cloudwatchLogsSubscriptions → snake_case cloudwatch_logs_subscriptions
|
||||
oldLogs := &OldAWSLogsStrategy{}
|
||||
for _, s := range awsCfg.TelemetryCollectionStrategy.Logs.Subscriptions {
|
||||
oldLogs.Subscriptions = append(oldLogs.Subscriptions, struct {
|
||||
LogGroupNamePrefix string `json:"log_group_name_prefix"`
|
||||
FilterPattern string `json:"filter_pattern"`
|
||||
}{LogGroupNamePrefix: s.LogGroupNamePrefix, FilterPattern: s.FilterPattern})
|
||||
}
|
||||
oldTelemetry.Logs = oldLogs
|
||||
}
|
||||
|
||||
older.Telemetry = oldTelemetry
|
||||
return older
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
|
||||
type ServiceID struct{ valuer.String }
|
||||
|
||||
var ErrCodeInvalidServiceID = errors.MustNewCode("invalid_service_id")
|
||||
|
||||
var (
|
||||
AWSServiceALB = ServiceID{valuer.NewString("alb")}
|
||||
AWSServiceAPIGateway = ServiceID{valuer.NewString("api-gateway")}
|
||||
@@ -60,16 +62,11 @@ var SupportedServices = map[CloudProviderType][]ServiceID{
|
||||
},
|
||||
}
|
||||
|
||||
// NewServiceID returns a new ServiceID from a string, validated against the supported services for the given cloud provider.
|
||||
func NewServiceID(provider CloudProviderType, service string) (ServiceID, error) {
|
||||
services, ok := SupportedServices[provider]
|
||||
if !ok {
|
||||
return ServiceID{}, errors.NewInvalidInputf(ErrCodeInvalidServiceID, "no services defined for cloud provider: %s", provider)
|
||||
}
|
||||
for _, s := range services {
|
||||
for _, s := range SupportedServices[provider] {
|
||||
if s.StringValue() == service {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return ServiceID{}, errors.NewInvalidInputf(ErrCodeInvalidServiceID, "invalid service id %q for cloud provider %s", service, provider)
|
||||
return ServiceID{}, errors.NewInvalidInputf(ErrCodeInvalidServiceID, "invalid service id %q for %s cloud provider", service, provider.StringValue())
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -25,6 +26,16 @@ const (
|
||||
AlertTypeExceptions AlertType = "EXCEPTIONS_BASED_ALERT"
|
||||
)
|
||||
|
||||
// Enum implements jsonschema.Enum; returns the acceptable values for AlertType.
|
||||
func (AlertType) Enum() []any {
|
||||
return []any{
|
||||
AlertTypeMetric,
|
||||
AlertTypeTraces,
|
||||
AlertTypeLogs,
|
||||
AlertTypeExceptions,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultSchemaVersion = "v1"
|
||||
SchemaVersionV2Alpha1 = "v2alpha1"
|
||||
@@ -38,14 +49,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,19 +597,24 @@ 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"`
|
||||
}
|
||||
|
||||
// GettableRule has info for an alerting rules.
|
||||
type GettableRule struct {
|
||||
Id string `json:"id"`
|
||||
State AlertState `json:"state"`
|
||||
Id string `json:"id" required:"true"`
|
||||
State AlertState `json:"state" required:"true"`
|
||||
PostableRule
|
||||
CreatedAt *time.Time `json:"createAt"`
|
||||
CreatedBy *string `json:"createBy"`
|
||||
UpdatedAt *time.Time `json:"updateAt"`
|
||||
UpdatedBy *string `json:"updateBy"`
|
||||
CreatedAt time.Time `json:"createAt" required:"true"`
|
||||
CreatedBy *string `json:"createBy" nullable:"true"`
|
||||
UpdatedAt time.Time `json:"updateAt" required:"true"`
|
||||
UpdatedBy *string `json:"updateBy" nullable:"true"`
|
||||
}
|
||||
|
||||
func (g *GettableRule) MarshalJSON() ([]byte, error) {
|
||||
@@ -625,3 +641,57 @@ func (g *GettableRule) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
}
|
||||
|
||||
// Rule is the v2 API read model for an alerting rule. It aligns audit fields
|
||||
// with the canonical types.TimeAuditable / types.UserAuditable shape used by
|
||||
// PlannedMaintenance and other entities. v1 handlers keep serializing
|
||||
// GettableRule directly for back-compat with existing SDK / Terraform clients.
|
||||
type Rule struct {
|
||||
Id string `json:"id" required:"true"`
|
||||
State AlertState `json:"state" required:"true"`
|
||||
PostableRule
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
}
|
||||
|
||||
func NewRule(g *GettableRule) *Rule {
|
||||
r := &Rule{
|
||||
Id: g.Id,
|
||||
State: g.State,
|
||||
PostableRule: g.PostableRule,
|
||||
}
|
||||
r.CreatedAt = g.CreatedAt
|
||||
r.UpdatedAt = g.UpdatedAt
|
||||
if g.CreatedBy != nil {
|
||||
r.CreatedBy = *g.CreatedBy
|
||||
}
|
||||
if g.UpdatedBy != nil {
|
||||
r.UpdatedBy = *g.UpdatedBy
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Rule) MarshalJSON() ([]byte, error) {
|
||||
type Alias Rule
|
||||
|
||||
switch r.SchemaVersion {
|
||||
case DefaultSchemaVersion:
|
||||
copyStruct := *r
|
||||
aux := Alias(copyStruct)
|
||||
if aux.RuleCondition != nil {
|
||||
aux.RuleCondition.Thresholds = nil
|
||||
}
|
||||
aux.Evaluation = nil
|
||||
aux.SchemaVersion = ""
|
||||
aux.NotificationSettings = nil
|
||||
return json.Marshal(aux)
|
||||
case SchemaVersionV2Alpha1:
|
||||
copyStruct := *r
|
||||
aux := Alias(copyStruct)
|
||||
return json.Marshal(aux)
|
||||
default:
|
||||
copyStruct := *r
|
||||
aux := Alias(copyStruct)
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,13 +55,13 @@ 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 {
|
||||
Type ScheduleType `json:"type"`
|
||||
Type ScheduleType `json:"type" required:"true"`
|
||||
Minute *int `json:"minute,omitempty"` // 0-59, for all types
|
||||
Hour *int `json:"hour,omitempty"` // 0-23, for daily/weekly/monthly
|
||||
Day *int `json:"day,omitempty"` // 1-31, for monthly
|
||||
@@ -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 {
|
||||
|
||||
@@ -15,6 +15,42 @@ var (
|
||||
ErrCodeInvalidPlannedMaintenancePayload = errors.MustNewCode("invalid_planned_maintenance_payload")
|
||||
)
|
||||
|
||||
type MaintenanceStatus struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
MaintenanceStatusActive = MaintenanceStatus{valuer.NewString("active")}
|
||||
MaintenanceStatusUpcoming = MaintenanceStatus{valuer.NewString("upcoming")}
|
||||
MaintenanceStatusExpired = MaintenanceStatus{valuer.NewString("expired")}
|
||||
)
|
||||
|
||||
// Enum implements jsonschema.Enum; returns the acceptable values for MaintenanceStatus.
|
||||
func (MaintenanceStatus) Enum() []any {
|
||||
return []any{
|
||||
MaintenanceStatusActive,
|
||||
MaintenanceStatusUpcoming,
|
||||
MaintenanceStatusExpired,
|
||||
}
|
||||
}
|
||||
|
||||
type MaintenanceKind struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
MaintenanceKindFixed = MaintenanceKind{valuer.NewString("fixed")}
|
||||
MaintenanceKindRecurring = MaintenanceKind{valuer.NewString("recurring")}
|
||||
)
|
||||
|
||||
// Enum implements jsonschema.Enum; returns the acceptable values for MaintenanceKind.
|
||||
func (MaintenanceKind) Enum() []any {
|
||||
return []any{
|
||||
MaintenanceKindFixed,
|
||||
MaintenanceKindRecurring,
|
||||
}
|
||||
}
|
||||
|
||||
type StorablePlannedMaintenance struct {
|
||||
bun.BaseModel `bun:"table:planned_maintenance"`
|
||||
types.Identifiable
|
||||
@@ -26,18 +62,63 @@ type StorablePlannedMaintenance struct {
|
||||
OrgID string `bun:"org_id,type:text"`
|
||||
}
|
||||
|
||||
type GettablePlannedMaintenance struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
type PlannedMaintenance struct {
|
||||
ID valuer.UUID `json:"id" required:"true"`
|
||||
Name string `json:"name" required:"true"`
|
||||
Description string `json:"description"`
|
||||
Schedule *Schedule `json:"schedule" required:"true"`
|
||||
RuleIDs []string `json:"alertIds"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
Status MaintenanceStatus `json:"status" required:"true"`
|
||||
Kind MaintenanceKind `json:"kind" required:"true"`
|
||||
}
|
||||
|
||||
// PostablePlannedMaintenance is the input payload for creating or updating a
|
||||
// planned maintenance. Server-owned fields (id, timestamps, audit users,
|
||||
// derived status / kind) are deliberately not accepted from the client.
|
||||
type PostablePlannedMaintenance struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
Description string `json:"description"`
|
||||
Schedule *Schedule `json:"schedule"`
|
||||
RuleIDs []string `json:"alertIds"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
Status string `json:"status"`
|
||||
Kind string `json:"kind"`
|
||||
Schedule *Schedule `json:"schedule" required:"true"`
|
||||
AlertIds []string `json:"alertIds"`
|
||||
}
|
||||
|
||||
func (p *PostablePlannedMaintenance) Validate() error {
|
||||
if p.Name == "" {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing name in the payload")
|
||||
}
|
||||
if p.Schedule == nil {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing schedule in the payload")
|
||||
}
|
||||
if p.Schedule.Timezone == "" {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing timezone in the payload")
|
||||
}
|
||||
|
||||
if _, err := time.LoadLocation(p.Schedule.Timezone); err != nil {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "invalid timezone in the payload")
|
||||
}
|
||||
|
||||
if !p.Schedule.StartTime.IsZero() && !p.Schedule.EndTime.IsZero() {
|
||||
if p.Schedule.StartTime.After(p.Schedule.EndTime) {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "start time cannot be after end time")
|
||||
}
|
||||
}
|
||||
|
||||
if p.Schedule.Recurrence != nil {
|
||||
if p.Schedule.Recurrence.RepeatType.IsZero() {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing repeat type in the payload")
|
||||
}
|
||||
if p.Schedule.Recurrence.Duration.IsZero() {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing duration in the payload")
|
||||
}
|
||||
if p.Schedule.Recurrence.EndTime != nil && p.Schedule.Recurrence.EndTime.Before(p.Schedule.Recurrence.StartTime) {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "end time cannot be before start time")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type StorablePlannedMaintenanceRule struct {
|
||||
@@ -47,12 +128,12 @@ type StorablePlannedMaintenanceRule struct {
|
||||
RuleID valuer.UUID `bun:"rule_id,type:text"`
|
||||
}
|
||||
|
||||
type GettablePlannedMaintenanceRule struct {
|
||||
type PlannedMaintenanceWithRules struct {
|
||||
*StorablePlannedMaintenance `bun:",extend"`
|
||||
Rules []*StorablePlannedMaintenanceRule `bun:"rel:has-many,join:id=planned_maintenance_id"`
|
||||
}
|
||||
|
||||
func (m *GettablePlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bool {
|
||||
func (m *PlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bool {
|
||||
// Check if the alert ID is in the maintenance window
|
||||
found := false
|
||||
if len(m.RuleIDs) > 0 {
|
||||
@@ -122,7 +203,7 @@ func (m *GettablePlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bo
|
||||
|
||||
// checkDaily rebases the recurrence start to today (or yesterday if needed)
|
||||
// and returns true if currentTime is within [candidate, candidate+Duration].
|
||||
func (m *GettablePlannedMaintenance) checkDaily(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
func (m *PlannedMaintenance) checkDaily(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
candidate := time.Date(
|
||||
currentTime.Year(), currentTime.Month(), currentTime.Day(),
|
||||
rec.StartTime.Hour(), rec.StartTime.Minute(), 0, 0,
|
||||
@@ -137,7 +218,7 @@ func (m *GettablePlannedMaintenance) checkDaily(currentTime time.Time, rec *Recu
|
||||
// checkWeekly finds the most recent allowed occurrence by rebasing the recurrence’s
|
||||
// time-of-day onto the allowed weekday. It does this for each allowed day and returns true
|
||||
// if the current time falls within the candidate window.
|
||||
func (m *GettablePlannedMaintenance) checkWeekly(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
func (m *PlannedMaintenance) checkWeekly(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
// If no days specified, treat as every day (like daily).
|
||||
if len(rec.RepeatOn) == 0 {
|
||||
return m.checkDaily(currentTime, rec, loc)
|
||||
@@ -169,7 +250,7 @@ func (m *GettablePlannedMaintenance) checkWeekly(currentTime time.Time, rec *Rec
|
||||
|
||||
// checkMonthly rebases the candidate occurrence using the recurrence's day-of-month.
|
||||
// If the candidate for the current month is in the future, it uses the previous month.
|
||||
func (m *GettablePlannedMaintenance) checkMonthly(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
func (m *PlannedMaintenance) checkMonthly(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
refDay := rec.StartTime.Day()
|
||||
year, month, _ := currentTime.Date()
|
||||
lastDay := time.Date(year, month+1, 0, 0, 0, 0, 0, loc).Day()
|
||||
@@ -201,7 +282,7 @@ func (m *GettablePlannedMaintenance) checkMonthly(currentTime time.Time, rec *Re
|
||||
return currentTime.Sub(candidate) <= rec.Duration.Duration()
|
||||
}
|
||||
|
||||
func (m *GettablePlannedMaintenance) IsActive(now time.Time) bool {
|
||||
func (m *PlannedMaintenance) IsActive(now time.Time) bool {
|
||||
ruleID := "maintenance"
|
||||
if len(m.RuleIDs) > 0 {
|
||||
ruleID = (m.RuleIDs)[0]
|
||||
@@ -209,7 +290,7 @@ func (m *GettablePlannedMaintenance) IsActive(now time.Time) bool {
|
||||
return m.ShouldSkip(ruleID, now)
|
||||
}
|
||||
|
||||
func (m *GettablePlannedMaintenance) IsUpcoming() bool {
|
||||
func (m *PlannedMaintenance) IsUpcoming() bool {
|
||||
loc, err := time.LoadLocation(m.Schedule.Timezone)
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -225,11 +306,11 @@ func (m *GettablePlannedMaintenance) IsUpcoming() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *GettablePlannedMaintenance) IsRecurring() bool {
|
||||
func (m *PlannedMaintenance) IsRecurring() bool {
|
||||
return m.Schedule.Recurrence != nil
|
||||
}
|
||||
|
||||
func (m *GettablePlannedMaintenance) Validate() error {
|
||||
func (m *PlannedMaintenance) Validate() error {
|
||||
if m.Name == "" {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing name in the payload")
|
||||
}
|
||||
@@ -252,7 +333,7 @@ func (m *GettablePlannedMaintenance) Validate() error {
|
||||
}
|
||||
|
||||
if m.Schedule.Recurrence != nil {
|
||||
if m.Schedule.Recurrence.RepeatType == "" {
|
||||
if m.Schedule.Recurrence.RepeatType.IsZero() {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing repeat type in the payload")
|
||||
}
|
||||
if m.Schedule.Recurrence.Duration.IsZero() {
|
||||
@@ -265,38 +346,38 @@ func (m *GettablePlannedMaintenance) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m GettablePlannedMaintenance) MarshalJSON() ([]byte, error) {
|
||||
func (m PlannedMaintenance) MarshalJSON() ([]byte, error) {
|
||||
now := time.Now().In(time.FixedZone(m.Schedule.Timezone, 0))
|
||||
var status string
|
||||
var status MaintenanceStatus
|
||||
if m.IsActive(now) {
|
||||
status = "active"
|
||||
status = MaintenanceStatusActive
|
||||
} else if m.IsUpcoming() {
|
||||
status = "upcoming"
|
||||
status = MaintenanceStatusUpcoming
|
||||
} else {
|
||||
status = "expired"
|
||||
status = MaintenanceStatusExpired
|
||||
}
|
||||
var kind string
|
||||
var kind MaintenanceKind
|
||||
|
||||
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() && m.Schedule.EndTime.After(m.Schedule.StartTime) {
|
||||
kind = "fixed"
|
||||
kind = MaintenanceKindFixed
|
||||
} else {
|
||||
kind = "recurring"
|
||||
kind = MaintenanceKindRecurring
|
||||
}
|
||||
|
||||
return json.Marshal(struct {
|
||||
Id string `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Schedule *Schedule `json:"schedule" db:"schedule"`
|
||||
AlertIds []string `json:"alertIds" db:"alert_ids"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
CreatedBy string `json:"createdBy" db:"created_by"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
UpdatedBy string `json:"updatedBy" db:"updated_by"`
|
||||
Status string `json:"status"`
|
||||
Kind string `json:"kind"`
|
||||
ID valuer.UUID `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Schedule *Schedule `json:"schedule" db:"schedule"`
|
||||
AlertIds []string `json:"alertIds" db:"alert_ids"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
CreatedBy string `json:"createdBy" db:"created_by"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
UpdatedBy string `json:"updatedBy" db:"updated_by"`
|
||||
Status MaintenanceStatus `json:"status"`
|
||||
Kind MaintenanceKind `json:"kind"`
|
||||
}{
|
||||
Id: m.Id,
|
||||
ID: m.ID,
|
||||
Name: m.Name,
|
||||
Description: m.Description,
|
||||
Schedule: m.Schedule,
|
||||
@@ -310,7 +391,7 @@ func (m GettablePlannedMaintenance) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
}
|
||||
|
||||
func (m *GettablePlannedMaintenanceRule) ConvertGettableMaintenanceRuleToGettableMaintenance() *GettablePlannedMaintenance {
|
||||
func (m *PlannedMaintenanceWithRules) ToPlannedMaintenance() *PlannedMaintenance {
|
||||
ruleIDs := []string{}
|
||||
if m.Rules != nil {
|
||||
for _, storableMaintenanceRule := range m.Rules {
|
||||
@@ -318,8 +399,8 @@ func (m *GettablePlannedMaintenanceRule) ConvertGettableMaintenanceRuleToGettabl
|
||||
}
|
||||
}
|
||||
|
||||
return &GettablePlannedMaintenance{
|
||||
Id: m.ID.StringValue(),
|
||||
return &PlannedMaintenance{
|
||||
ID: m.ID,
|
||||
Name: m.Name,
|
||||
Description: m.Description,
|
||||
Schedule: m.Schedule,
|
||||
@@ -331,10 +412,15 @@ func (m *GettablePlannedMaintenanceRule) ConvertGettableMaintenanceRuleToGettabl
|
||||
}
|
||||
}
|
||||
|
||||
type MaintenanceStore interface {
|
||||
CreatePlannedMaintenance(context.Context, GettablePlannedMaintenance) (valuer.UUID, error)
|
||||
DeletePlannedMaintenance(context.Context, valuer.UUID) error
|
||||
GetPlannedMaintenanceByID(context.Context, valuer.UUID) (*GettablePlannedMaintenance, error)
|
||||
EditPlannedMaintenance(context.Context, GettablePlannedMaintenance, valuer.UUID) error
|
||||
GetAllPlannedMaintenance(context.Context, string) ([]*GettablePlannedMaintenance, error)
|
||||
type ListPlannedMaintenanceParams struct {
|
||||
Active *bool `query:"active"`
|
||||
Recurring *bool `query:"recurring"`
|
||||
}
|
||||
|
||||
type MaintenanceStore interface {
|
||||
CreatePlannedMaintenance(context.Context, *PostablePlannedMaintenance) (*PlannedMaintenance, error)
|
||||
DeletePlannedMaintenance(context.Context, valuer.UUID) error
|
||||
GetPlannedMaintenanceByID(context.Context, valuer.UUID) (*PlannedMaintenance, error)
|
||||
UpdatePlannedMaintenance(context.Context, *PostablePlannedMaintenance, valuer.UUID) error
|
||||
ListPlannedMaintenance(context.Context, string) ([]*PlannedMaintenance, error)
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
maintenance *GettablePlannedMaintenance
|
||||
maintenance *PlannedMaintenance
|
||||
ts time.Time
|
||||
skip bool
|
||||
}{
|
||||
{
|
||||
name: "only-on-saturday",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "Europe/London",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -39,7 +39,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Testing weekly recurrence with midnight crossing
|
||||
{
|
||||
name: "weekly-across-midnight-previous-day",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -56,7 +56,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Testing weekly recurrence with midnight crossing
|
||||
{
|
||||
name: "weekly-across-midnight-previous-day",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -73,7 +73,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Testing weekly recurrence with multi day duration
|
||||
{
|
||||
name: "weekly-across-midnight-previous-day",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -90,7 +90,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Weekly recurrence where the previous day is not in RepeatOn
|
||||
{
|
||||
name: "weekly-across-midnight-previous-day-not-in-repeaton",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -107,7 +107,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Daily recurrence with midnight crossing
|
||||
{
|
||||
name: "daily-maintenance-across-midnight",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -123,7 +123,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Exactly at start time boundary
|
||||
{
|
||||
name: "at-start-time-boundary",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -139,7 +139,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Exactly at end time boundary
|
||||
{
|
||||
name: "at-end-time-boundary",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -155,7 +155,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Monthly maintenance with multi-day duration
|
||||
{
|
||||
name: "monthly-multi-day-duration",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -171,7 +171,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Weekly maintenance with multi-day duration
|
||||
{
|
||||
name: "weekly-multi-day-duration",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -188,7 +188,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Monthly maintenance that crosses to next month
|
||||
{
|
||||
name: "monthly-crosses-to-next-month",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -204,7 +204,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Different timezone tests
|
||||
{
|
||||
name: "timezone-offset-test",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "America/New_York", // UTC-5 or UTC-4 depending on DST
|
||||
Recurrence: &Recurrence{
|
||||
@@ -220,7 +220,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Test negative case - time well outside window
|
||||
{
|
||||
name: "daily-maintenance-time-outside-window",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -236,7 +236,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Test for recurring maintenance with an end date that is before the current time
|
||||
{
|
||||
name: "recurring-maintenance-with-past-end-date",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -253,7 +253,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Monthly recurring maintenance spanning end of month into beginning of next month
|
||||
{
|
||||
name: "monthly-maintenance-spans-month-end",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -269,7 +269,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Test for RepeatOn with empty array (should apply to all days)
|
||||
{
|
||||
name: "weekly-empty-repeaton",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -286,7 +286,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// February has fewer days than January - test the edge case when maintenance is on 31st
|
||||
{
|
||||
name: "monthly-maintenance-february-fewer-days",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -301,7 +301,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "daily-maintenance-crosses-midnight",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -316,7 +316,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "monthly-maintenance-crosses-month-end",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -331,7 +331,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-days",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -346,7 +346,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "weekly-maintenance-crosses-midnight",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -362,7 +362,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-days",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -377,7 +377,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "daily-maintenance-crosses-midnight",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -392,7 +392,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-hours",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -407,7 +407,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "fixed planned maintenance start <= ts <= end",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
StartTime: time.Now().UTC().Add(-time.Hour),
|
||||
@@ -419,7 +419,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "fixed planned maintenance start >= ts",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
StartTime: time.Now().UTC().Add(time.Hour),
|
||||
@@ -431,7 +431,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "fixed planned maintenance ts < start",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
StartTime: time.Now().UTC().Add(time.Hour),
|
||||
@@ -443,7 +443,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat sunday, saturday, weekly for 24 hours, in Us/Eastern timezone",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "US/Eastern",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -459,7 +459,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -474,7 +474,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -489,7 +489,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -504,7 +504,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -520,7 +520,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -536,7 +536,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -552,7 +552,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -568,7 +568,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -584,7 +584,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -599,7 +599,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -614,7 +614,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
|
||||
@@ -8,26 +8,52 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type RepeatType string
|
||||
type RepeatType struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
const (
|
||||
RepeatTypeDaily RepeatType = "daily"
|
||||
RepeatTypeWeekly RepeatType = "weekly"
|
||||
RepeatTypeMonthly RepeatType = "monthly"
|
||||
var (
|
||||
RepeatTypeDaily = RepeatType{valuer.NewString("daily")}
|
||||
RepeatTypeWeekly = RepeatType{valuer.NewString("weekly")}
|
||||
RepeatTypeMonthly = RepeatType{valuer.NewString("monthly")}
|
||||
)
|
||||
|
||||
type RepeatOn string
|
||||
// Enum implements jsonschema.Enum; returns the acceptable values for RepeatType.
|
||||
func (RepeatType) Enum() []any {
|
||||
return []any{
|
||||
RepeatTypeDaily,
|
||||
RepeatTypeWeekly,
|
||||
RepeatTypeMonthly,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
RepeatOnSunday RepeatOn = "sunday"
|
||||
RepeatOnMonday RepeatOn = "monday"
|
||||
RepeatOnTuesday RepeatOn = "tuesday"
|
||||
RepeatOnWednesday RepeatOn = "wednesday"
|
||||
RepeatOnThursday RepeatOn = "thursday"
|
||||
RepeatOnFriday RepeatOn = "friday"
|
||||
RepeatOnSaturday RepeatOn = "saturday"
|
||||
type RepeatOn struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
RepeatOnSunday = RepeatOn{valuer.NewString("sunday")}
|
||||
RepeatOnMonday = RepeatOn{valuer.NewString("monday")}
|
||||
RepeatOnTuesday = RepeatOn{valuer.NewString("tuesday")}
|
||||
RepeatOnWednesday = RepeatOn{valuer.NewString("wednesday")}
|
||||
RepeatOnThursday = RepeatOn{valuer.NewString("thursday")}
|
||||
RepeatOnFriday = RepeatOn{valuer.NewString("friday")}
|
||||
RepeatOnSaturday = RepeatOn{valuer.NewString("saturday")}
|
||||
)
|
||||
|
||||
// Enum implements jsonschema.Enum; returns the acceptable values for RepeatOn.
|
||||
func (RepeatOn) Enum() []any {
|
||||
return []any{
|
||||
RepeatOnSunday,
|
||||
RepeatOnMonday,
|
||||
RepeatOnTuesday,
|
||||
RepeatOnWednesday,
|
||||
RepeatOnThursday,
|
||||
RepeatOnFriday,
|
||||
RepeatOnSaturday,
|
||||
}
|
||||
}
|
||||
|
||||
var RepeatOnAllMap = map[RepeatOn]time.Weekday{
|
||||
RepeatOnSunday: time.Sunday,
|
||||
RepeatOnMonday: time.Monday,
|
||||
@@ -39,10 +65,10 @@ var RepeatOnAllMap = map[RepeatOn]time.Weekday{
|
||||
}
|
||||
|
||||
type Recurrence struct {
|
||||
StartTime time.Time `json:"startTime"`
|
||||
StartTime time.Time `json:"startTime" required:"true"`
|
||||
EndTime *time.Time `json:"endTime,omitempty"`
|
||||
Duration valuer.TextDuration `json:"duration"`
|
||||
RepeatType RepeatType `json:"repeatType"`
|
||||
Duration valuer.TextDuration `json:"duration" required:"true"`
|
||||
RepeatType RepeatType `json:"repeatType" required:"true"`
|
||||
RepeatOn []RepeatOn `json:"repeatOn"`
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type Rule struct {
|
||||
type StorableRule struct {
|
||||
bun.BaseModel `bun:"table:rule"`
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
@@ -20,7 +20,7 @@ type Rule struct {
|
||||
OrgID string `bun:"org_id,type:text"`
|
||||
}
|
||||
|
||||
func NewStatsFromRules(rules []*Rule) map[string]any {
|
||||
func NewStatsFromRules(rules []*StorableRule) map[string]any {
|
||||
stats := make(map[string]any)
|
||||
for _, rule := range rules {
|
||||
gettableRule := &GettableRule{}
|
||||
@@ -54,10 +54,10 @@ type RuleAlert struct {
|
||||
}
|
||||
|
||||
type RuleStore interface {
|
||||
CreateRule(context.Context, *Rule, func(context.Context, valuer.UUID) error) (valuer.UUID, error)
|
||||
EditRule(context.Context, *Rule, func(context.Context) error) error
|
||||
CreateRule(context.Context, *StorableRule, func(context.Context, valuer.UUID) error) (valuer.UUID, error)
|
||||
EditRule(context.Context, *StorableRule, func(context.Context) error) error
|
||||
DeleteRule(context.Context, valuer.UUID, func(context.Context) error) error
|
||||
GetStoredRules(context.Context, string) ([]*Rule, error)
|
||||
GetStoredRule(context.Context, valuer.UUID) (*Rule, error)
|
||||
GetStoredRules(context.Context, string) ([]*StorableRule, error)
|
||||
GetStoredRule(context.Context, valuer.UUID) (*StorableRule, error)
|
||||
GetStoredRulesByMetricName(context.Context, string, string) ([]RuleAlert, 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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,11 @@ import (
|
||||
type Config struct {
|
||||
// Whether the web package is enabled.
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
// The prefix to serve the files from
|
||||
Prefix string `mapstructure:"prefix"`
|
||||
// The directory containing the static build files. The root of this directory should
|
||||
// have an index.html file.
|
||||
|
||||
// The name of the index file to serve.
|
||||
Index string `mapstructure:"index"`
|
||||
|
||||
// The directory from which to serve the web files.
|
||||
Directory string `mapstructure:"directory"`
|
||||
}
|
||||
|
||||
@@ -22,7 +23,7 @@ func NewConfigFactory() factory.ConfigFactory {
|
||||
func newConfig() factory.Config {
|
||||
return &Config{
|
||||
Enabled: true,
|
||||
Prefix: "/",
|
||||
Index: "index.html",
|
||||
Directory: "/etc/signoz/web",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
)
|
||||
|
||||
func TestNewWithEnvProvider(t *testing.T) {
|
||||
t.Setenv("SIGNOZ_WEB_PREFIX", "/web")
|
||||
t.Setenv("SIGNOZ_WEB_ENABLED", "false")
|
||||
|
||||
conf, err := config.New(
|
||||
@@ -37,7 +36,7 @@ func TestNewWithEnvProvider(t *testing.T) {
|
||||
|
||||
expected := &Config{
|
||||
Enabled: false,
|
||||
Prefix: "/web",
|
||||
Index: def.Index,
|
||||
Directory: def.Directory,
|
||||
}
|
||||
|
||||
|
||||
@@ -8,56 +8,55 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/web"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const (
|
||||
indexFileName string = "index.html"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
config web.Config
|
||||
config web.Config
|
||||
indexContents []byte
|
||||
fileHandler http.Handler
|
||||
}
|
||||
|
||||
func NewFactory() factory.ProviderFactory[web.Web, web.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("router"), New)
|
||||
func NewFactory(globalConfig global.Config) factory.ProviderFactory[web.Web, web.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("router"), func(ctx context.Context, settings factory.ProviderSettings, config web.Config) (web.Web, error) {
|
||||
return New(ctx, settings, config, globalConfig)
|
||||
})
|
||||
}
|
||||
|
||||
func New(ctx context.Context, settings factory.ProviderSettings, config web.Config) (web.Web, error) {
|
||||
func New(ctx context.Context, settings factory.ProviderSettings, config web.Config, globalConfig global.Config) (web.Web, error) {
|
||||
fi, err := os.Stat(config.Directory)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot access web directory")
|
||||
}
|
||||
|
||||
ok := fi.IsDir()
|
||||
if !ok {
|
||||
if !fi.IsDir() {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "web directory is not a directory")
|
||||
}
|
||||
|
||||
fi, err = os.Stat(filepath.Join(config.Directory, indexFileName))
|
||||
indexPath := filepath.Join(config.Directory, config.Index)
|
||||
raw, err := os.ReadFile(indexPath)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot access %q in web directory", indexFileName)
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot read %q in web directory", config.Index)
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) || fi.IsDir() {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "%q does not exist", indexFileName)
|
||||
}
|
||||
logger := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/web/routerweb").Logger()
|
||||
indexContents := web.NewIndex(ctx, logger, config.Index, raw, web.TemplateData{BaseHref: globalConfig.ExternalPathTrailing()})
|
||||
|
||||
return &provider{
|
||||
config: config,
|
||||
config: config,
|
||||
indexContents: indexContents,
|
||||
fileHandler: http.FileServer(http.Dir(config.Directory)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
cache := middleware.NewCache(0)
|
||||
err := router.PathPrefix(provider.config.Prefix).
|
||||
err := router.PathPrefix("/").
|
||||
Handler(
|
||||
http.StripPrefix(
|
||||
provider.config.Prefix,
|
||||
cache.Wrap(http.HandlerFunc(provider.ServeHTTP)),
|
||||
),
|
||||
cache.Wrap(http.HandlerFunc(provider.ServeHTTP)),
|
||||
).GetError()
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "unable to add web to router")
|
||||
@@ -75,7 +74,7 @@ func (provider *provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if err != nil {
|
||||
// if the file doesn't exist, serve index.html
|
||||
if os.IsNotExist(err) {
|
||||
http.ServeFile(rw, req, filepath.Join(provider.config.Directory, indexFileName))
|
||||
provider.serveIndex(rw)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -87,10 +86,15 @@ func (provider *provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
if fi.IsDir() {
|
||||
// path is a directory, serve index.html
|
||||
http.ServeFile(rw, req, filepath.Join(provider.config.Directory, indexFileName))
|
||||
provider.serveIndex(rw)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, use http.FileServer to serve the static file
|
||||
http.FileServer(http.Dir(provider.config.Directory)).ServeHTTP(rw, req)
|
||||
provider.fileHandler.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func (provider *provider) serveIndex(rw http.ResponseWriter) {
|
||||
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = rw.Write(provider.indexContents)
|
||||
}
|
||||
|
||||
@@ -5,45 +5,113 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/web"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServeHttpWithoutPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
fi, err := os.Open(filepath.Join("testdata", indexFileName))
|
||||
require.NoError(t, err)
|
||||
func startServer(t *testing.T, config web.Config, globalConfig global.Config) string {
|
||||
t.Helper()
|
||||
|
||||
expected, err := io.ReadAll(fi)
|
||||
require.NoError(t, err)
|
||||
|
||||
web, err := New(context.Background(), factorytest.NewSettings(), web.Config{Prefix: "/", Directory: filepath.Join("testdata")})
|
||||
web, err := New(context.Background(), factorytest.NewSettings(), config, globalConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
router := mux.NewRouter()
|
||||
err = web.AddToRouter(router)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, web.AddToRouter(router))
|
||||
|
||||
listener, err := net.Listen("tcp", "localhost:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
server := &http.Server{
|
||||
Handler: router,
|
||||
server := &http.Server{Handler: router}
|
||||
go func() { _ = server.Serve(listener) }()
|
||||
t.Cleanup(func() { _ = server.Close() })
|
||||
|
||||
return "http://" + listener.Addr().String()
|
||||
}
|
||||
|
||||
func httpGet(t *testing.T, url string) string {
|
||||
t.Helper()
|
||||
|
||||
res, err := http.DefaultClient.Get(url)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func TestServeTemplatedIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
path string
|
||||
globalConfig global.Config
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "RootBaseHrefAtRoot",
|
||||
path: "/",
|
||||
globalConfig: global.Config{},
|
||||
expected: `<html><head><base href="/" /></head><body>Welcome to test data!!!</body></html>`,
|
||||
},
|
||||
{
|
||||
name: "RootBaseHrefAtNonExistentPath",
|
||||
path: "/does-not-exist",
|
||||
globalConfig: global.Config{},
|
||||
expected: `<html><head><base href="/" /></head><body>Welcome to test data!!!</body></html>`,
|
||||
},
|
||||
{
|
||||
name: "RootBaseHrefAtDirectory",
|
||||
path: "/assets",
|
||||
globalConfig: global.Config{},
|
||||
expected: `<html><head><base href="/" /></head><body>Welcome to test data!!!</body></html>`,
|
||||
},
|
||||
{
|
||||
name: "SubPathBaseHrefAtRoot",
|
||||
path: "/",
|
||||
globalConfig: global.Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
|
||||
expected: `<html><head><base href="/signoz/" /></head><body>Welcome to test data!!!</body></html>`,
|
||||
},
|
||||
{
|
||||
name: "SubPathBaseHrefAtNonExistentPath",
|
||||
path: "/does-not-exist",
|
||||
globalConfig: global.Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
|
||||
expected: `<html><head><base href="/signoz/" /></head><body>Welcome to test data!!!</body></html>`,
|
||||
},
|
||||
{
|
||||
name: "SubPathBaseHrefAtDirectory",
|
||||
path: "/assets",
|
||||
globalConfig: global.Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
|
||||
expected: `<html><head><base href="/signoz/" /></head><body>Welcome to test data!!!</body></html>`,
|
||||
},
|
||||
}
|
||||
|
||||
go func() {
|
||||
_ = server.Serve(listener)
|
||||
}()
|
||||
defer func() {
|
||||
_ = server.Close()
|
||||
}()
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
base := startServer(t, web.Config{Index: "valid_template.html", Directory: "testdata"}, testCase.globalConfig)
|
||||
|
||||
assert.Equal(t, testCase.expected, strings.TrimSuffix(httpGet(t, base+testCase.path), "\n"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeNoTemplateIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expected, err := os.ReadFile(filepath.Join("testdata", "no_template.html"))
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -54,11 +122,7 @@ func TestServeHttpWithoutPrefix(t *testing.T) {
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
name: "Index",
|
||||
path: "/" + indexFileName,
|
||||
},
|
||||
{
|
||||
name: "DoesNotExist",
|
||||
name: "NonExistentPath",
|
||||
path: "/does-not-exist",
|
||||
},
|
||||
{
|
||||
@@ -67,104 +131,55 @@ func TestServeHttpWithoutPrefix(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res, err := http.DefaultClient.Get("http://" + listener.Addr().String() + tc.path)
|
||||
require.NoError(t, err)
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
base := startServer(t, web.Config{Index: "no_template.html", Directory: "testdata"}, global.Config{})
|
||||
|
||||
defer func() {
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
|
||||
actual, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
assert.Equal(t, string(expected), httpGet(t, base+testCase.path))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestServeHttpWithPrefix(t *testing.T) {
|
||||
func TestServeInvalidTemplateIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
fi, err := os.Open(filepath.Join("testdata", indexFileName))
|
||||
|
||||
expected, err := os.ReadFile(filepath.Join("testdata", "invalid_template.html"))
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, err := io.ReadAll(fi)
|
||||
require.NoError(t, err)
|
||||
|
||||
web, err := New(context.Background(), factorytest.NewSettings(), web.Config{Prefix: "/web", Directory: filepath.Join("testdata")})
|
||||
require.NoError(t, err)
|
||||
|
||||
router := mux.NewRouter()
|
||||
err = web.AddToRouter(router)
|
||||
require.NoError(t, err)
|
||||
|
||||
listener, err := net.Listen("tcp", "localhost:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
server := &http.Server{
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
go func() {
|
||||
_ = server.Serve(listener)
|
||||
}()
|
||||
defer func() {
|
||||
_ = server.Close()
|
||||
}()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
path string
|
||||
found bool
|
||||
name string
|
||||
path string
|
||||
}{
|
||||
{
|
||||
name: "Root",
|
||||
path: "/web",
|
||||
found: true,
|
||||
name: "Root",
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
name: "Index",
|
||||
path: "/web/" + indexFileName,
|
||||
found: true,
|
||||
name: "NonExistentPath",
|
||||
path: "/does-not-exist",
|
||||
},
|
||||
{
|
||||
name: "FileDoesNotExist",
|
||||
path: "/web/does-not-exist",
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "Directory",
|
||||
path: "/web/assets",
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "DoesNotExist",
|
||||
path: "/does-not-exist",
|
||||
found: false,
|
||||
name: "Directory",
|
||||
path: "/assets",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res, err := http.DefaultClient.Get("http://" + listener.Addr().String() + tc.path)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
|
||||
if tc.found {
|
||||
actual, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
} else {
|
||||
assert.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
base := startServer(t, web.Config{Index: "invalid_template.html", Directory: "testdata"}, global.Config{ExternalURL: &url.URL{Path: "/signoz"}})
|
||||
|
||||
assert.Equal(t, string(expected), httpGet(t, base+testCase.path))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestServeStaticFilesUnchanged(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expected, err := os.ReadFile(filepath.Join("testdata", "assets", "style.css"))
|
||||
require.NoError(t, err)
|
||||
|
||||
base := startServer(t, web.Config{Index: "valid_template.html", Directory: "testdata"}, global.Config{ExternalURL: &url.URL{Path: "/signoz"}})
|
||||
|
||||
assert.Equal(t, string(expected), httpGet(t, base+"/assets/style.css"))
|
||||
}
|
||||
|
||||
3
pkg/web/routerweb/testdata/assets/index.css
vendored
3
pkg/web/routerweb/testdata/assets/index.css
vendored
@@ -1,3 +0,0 @@
|
||||
#root {
|
||||
background-color: red;
|
||||
}
|
||||
1
pkg/web/routerweb/testdata/assets/style.css
vendored
Normal file
1
pkg/web/routerweb/testdata/assets/style.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
body { color: red; }
|
||||
1
pkg/web/routerweb/testdata/index.html
vendored
1
pkg/web/routerweb/testdata/index.html
vendored
@@ -1 +0,0 @@
|
||||
<h1>Welcome to test data!!!</h1>
|
||||
1
pkg/web/routerweb/testdata/invalid_template.html
vendored
Normal file
1
pkg/web/routerweb/testdata/invalid_template.html
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<html><head><base href="[[." /></head><body>Bad template</body></html>
|
||||
1
pkg/web/routerweb/testdata/no_template.html
vendored
Normal file
1
pkg/web/routerweb/testdata/no_template.html
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<html><head></head><body>No template here</body></html>
|
||||
1
pkg/web/routerweb/testdata/valid_template.html
vendored
Normal file
1
pkg/web/routerweb/testdata/valid_template.html
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<html><head><base href="[[.BaseHref]]" /></head><body>Welcome to test data!!!</body></html>
|
||||
42
pkg/web/template.go
Normal file
42
pkg/web/template.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
// Field names map to the HTML attributes they populate in the template:
|
||||
// - BaseHref → <base href="[[.BaseHref]]" />
|
||||
type TemplateData struct {
|
||||
BaseHref string
|
||||
}
|
||||
|
||||
// If the template cannot be parsed or executed, the raw bytes are
|
||||
// returned unchanged and the error is logged.
|
||||
func NewIndex(ctx context.Context, logger *slog.Logger, name string, raw []byte, data TemplateData) []byte {
|
||||
result, err := NewIndexE(name, raw, data)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "cannot render index template, serving raw file", slog.String("name", name), errors.Attr(err))
|
||||
return raw
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func NewIndexE(name string, raw []byte, data TemplateData) ([]byte, error) {
|
||||
tmpl, err := template.New(name).Delims("[[", "]]").Parse(string(raw))
|
||||
if err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot parse %q as template", name)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot execute template for %q", name)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user