mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-18 18:00:27 +01:00
Compare commits
1 Commits
docs/go-ty
...
ns/td-cach
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de8f3cc06e |
@@ -7,7 +7,6 @@ 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"
|
||||
@@ -15,7 +14,6 @@ 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"
|
||||
@@ -28,20 +26,14 @@ 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"
|
||||
@@ -83,7 +75,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
},
|
||||
signoz.NewEmailingProviderFactories(),
|
||||
signoz.NewCacheProviderFactories(),
|
||||
signoz.NewWebProviderFactories(config.Global),
|
||||
signoz.NewWebProviderFactories(),
|
||||
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
|
||||
return signoz.NewSQLSchemaProviderFactories(sqlstore)
|
||||
},
|
||||
@@ -115,9 +107,6 @@ 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,17 +22,14 @@ 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"
|
||||
@@ -43,21 +40,15 @@ 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"
|
||||
)
|
||||
@@ -105,7 +96,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
},
|
||||
signoz.NewEmailingProviderFactories(),
|
||||
signoz.NewCacheProviderFactories(),
|
||||
signoz.NewWebProviderFactories(config.Global),
|
||||
signoz.NewWebProviderFactories(),
|
||||
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 {
|
||||
@@ -175,9 +166,6 @@ 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,8 +6,6 @@
|
||||
##################### 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>
|
||||
@@ -52,8 +50,8 @@ pprof:
|
||||
web:
|
||||
# Whether to enable the web frontend
|
||||
enabled: true
|
||||
# The index file to use as the SPA entrypoint.
|
||||
index: index.html
|
||||
# The prefix to serve web on
|
||||
prefix: /
|
||||
# The directory containing the static build files.
|
||||
directory: /etc/signoz/web
|
||||
|
||||
|
||||
1152
docs/api/openapi.yml
1152
docs/api/openapi.yml
File diff suppressed because it is too large
Load Diff
@@ -20,4 +20,3 @@ We **recommend** (almost enforce) reviewing these guides before contributing to
|
||||
- [Packages](packages.md) - Naming, layout, and conventions for `pkg/` packages
|
||||
- [Service](service.md) - Managed service lifecycle with `factory.Service`
|
||||
- [SQL](sql.md) - Database and SQL patterns
|
||||
- [Types](types.md) - Domain types, request/response bodies, and storage rows in `pkg/types/`
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
# Types
|
||||
|
||||
Domain types in `pkg/types/<domain>/` live on three serialization boundaries — inbound HTTP, outbound HTTP, and SQL — on top of an in-memory domain representation. SigNoz's convention is **core-type-first**: every domain defines a single canonical type `X`, and specialized flavors (`PostableX`, `GettableX`, `UpdatableX`, `StorableX`) are introduced **only when they actually differ from `X`**. This guide spells out when each flavor is warranted and how they relate to each other.
|
||||
|
||||
Before reading, make sure you have read [abstractions.md](abstractions.md) — the rules here build on its guidance that every new type must earn its place.
|
||||
|
||||
## The core type is required
|
||||
|
||||
Every domain package in `pkg/types/<domain>/` defines exactly one core type `X`: `AuthDomain`, `Channel`, `Rule`, `Dashboard`, `Role`, `PlannedMaintenance`. This is the canonical in-memory representation of the domain object. Domain methods, validation invariants, and business logic hang off `X` — not off the flavor types.
|
||||
|
||||
Two rules shape how the core type behaves:
|
||||
|
||||
- **Conversions can be either `New<Output>From<Input>` or a receiver-style `(x *X) ToY()` method.** Either form is fine; pick whichever reads best at the call site:
|
||||
|
||||
```go
|
||||
// Constructor form
|
||||
func NewGettableAuthDomainFromAuthDomain(d *AuthDomain, info *AuthNProviderInfo) *GettableAuthDomain
|
||||
|
||||
// Receiver form
|
||||
func (m *PlannedMaintenanceWithRules) ToPlannedMaintenance() *PlannedMaintenance
|
||||
```
|
||||
- **`X` can double as the storage row** when the DB shape would be identical. `Channel` embeds `bun.BaseModel` directly, and there is no `StorableChannel`. This is the preferred shape when it works.
|
||||
|
||||
Domain packages under `pkg/types/` must not import from other `pkg/` packages. Keep the core type's methods lightweight and push orchestration out to the module layer.
|
||||
|
||||
## Add a flavor only when it differs
|
||||
|
||||
For each of the four flavors, create it only if its shape diverges from `X`. If a flavor would have the same fields and tags as `X`, reuse `X` directly, or declare a type alias. Every flavor must earn its place per [abstractions.md](abstractions.md) rule 6 ("Wrappers must add semantics, not just rename").
|
||||
|
||||
| Flavor | Create it when it differs in… |
|
||||
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `PostableX` | JSON shape differs from `X` — typically no `Id`, no audit fields, no server-computed fields. Often owns input validation via `Validate()` or a custom `UnmarshalJSON`. |
|
||||
| `GettableX` | Response shape adds server-computed fields that are not persisted — e.g., `GettableAuthDomain` adds `AuthNProviderInfo`, which is resolved at read time. |
|
||||
| `UpdatableX` | Only a strict subset of `PostableX` is replaceable on PUT. If the updatable shape equals `PostableX`, reuse `PostableX`. |
|
||||
| `StorableX` | DB row shape differs from `X` — usually `X` carries nested typed config while `StorableX` carries a flat `Data string` JSON column, plus bun tags, audit mixins, and an `OrgID`. If `X` already has those, skip the flavor. |
|
||||
|
||||
The failure mode this rule exists to prevent: minting all four flavors on reflex for every new resource, even when two or three are structurally identical. Each unnecessary flavor is another type contributors must understand and another conversion that can drift.
|
||||
|
||||
## Worked examples
|
||||
|
||||
### Channel — core type only
|
||||
|
||||
```go
|
||||
type Channels = []*Channel
|
||||
type GettableChannels = []*Channel
|
||||
|
||||
type Channel struct {
|
||||
bun.BaseModel `bun:"table:notification_channel"`
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Name string `json:"name" required:"true" bun:"name"`
|
||||
Type string `json:"type" required:"true" bun:"type"`
|
||||
Data string `json:"data" required:"true" bun:"data"`
|
||||
OrgID string `json:"orgId" required:"true" bun:"org_id"`
|
||||
}
|
||||
```
|
||||
|
||||
`Channel` is both the domain type and the bun row. `GettableChannels` is a **type alias** because `*Channel` already serializes correctly as a response. There is no `StorableChannel`, `PostableChannel`, or `UpdatableChannel` — those would be identical to `Channel` and so do not exist. Prefer this shape when it works.
|
||||
|
||||
### AuthDomain — all four flavors
|
||||
|
||||
```go
|
||||
type AuthDomain struct {
|
||||
storableAuthDomain *StorableAuthDomain
|
||||
authDomainConfig *AuthDomainConfig
|
||||
}
|
||||
|
||||
type StorableAuthDomain struct {
|
||||
bun.BaseModel `bun:"table:auth_domain"`
|
||||
types.Identifiable
|
||||
Name string `bun:"name"`
|
||||
Data string `bun:"data"` // AuthDomainConfig serialized as JSON
|
||||
OrgID valuer.UUID `bun:"org_id"`
|
||||
types.TimeAuditable
|
||||
}
|
||||
|
||||
type PostableAuthDomain struct {
|
||||
Config AuthDomainConfig `json:"config"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type UpdateableAuthDomain struct {
|
||||
Config AuthDomainConfig `json:"config"` // Name intentionally absent
|
||||
}
|
||||
|
||||
type GettableAuthDomain struct {
|
||||
*StorableAuthDomain
|
||||
*AuthDomainConfig
|
||||
AuthNProviderInfo *AuthNProviderInfo `json:"authNProviderInfo"`
|
||||
}
|
||||
```
|
||||
|
||||
Each flavor exists for a concrete reason:
|
||||
|
||||
- `StorableAuthDomain` stores the typed config as an opaque `Data string` column, so the schema does not need to migrate every time a config field is added.
|
||||
- `PostableAuthDomain` carries the config as a structured object (not a string) for the request.
|
||||
- `UpdateableAuthDomain` excludes `Name` because a domain's name cannot change after creation.
|
||||
- `GettableAuthDomain` adds `AuthNProviderInfo`, which is derived at read time and never persisted.
|
||||
|
||||
The core `AuthDomain` holds the two live halves — `storableAuthDomain` and `authDomainConfig` — and owns business methods such as `Update(config)`. Conversions use the `New<Output>From<Input>` form: `NewAuthDomainFromConfig`, `NewAuthDomainFromStorableAuthDomain`, `NewGettableAuthDomainFromAuthDomain`.
|
||||
|
||||
## Conventions that tie the flavors together
|
||||
|
||||
- **Conversions** use either a `New<Output>From<Input>` constructor — e.g. `NewChannelFromReceiver`, `NewGettableAuthDomainFromAuthDomain` — or a receiver-style `ToY()` method. Both forms coexist in the codebase; use whichever fits the call site.
|
||||
- **Validation belongs on the core type `X`.** Putting it on `X` means every write path — HTTP create, HTTP update, in-process migration, replay — runs the same checks. `Validate()` on `PostableX` is reserved for checks that are specific to the request shape and do not apply to `X`. `UnmarshalJSON` on `PostableX` is a separate tool that lives there because decoding only happens at the HTTP boundary — `PostableAuthDomain.UnmarshalJSON` rejecting a malformed domain name at decode time is the canonical example.
|
||||
|
||||
```go
|
||||
// Domain invariants: every write path re-runs these.
|
||||
func (x *X) Validate() error { ... }
|
||||
|
||||
// Request-shape-only: checks that do not apply once the value is persisted.
|
||||
func (p *PostableX) Validate() error { ... }
|
||||
```
|
||||
- **Type aliases, not wrappers**, when two shapes are identical. `type GettableChannels = []*Channel` is correct because it adds no semantics beyond the underlying type.
|
||||
- **Serialization tags** follow [handler.md](handler.md): `required:"true"` means the JSON key must be present, `nullable:"true"` is required on any slice or map that may serialize as `null`, and types with a fixed value set must implement `Enum() []any`.
|
||||
|
||||
## A note on `UpdatableX` and `PatchableX`
|
||||
|
||||
- `UpdatableX` — the body for PUT (full replace) when the shape is a strict subset of `PostableX`. If the updatable shape equals `PostableX`, reuse `PostableX`.
|
||||
- `PatchableX` — the body for PATCH (partial update); only the fields a client is allowed to patch. For example, `PatchableRole` carries a single `Description` field even though `Role` has many — clients may patch the description but not anything else.
|
||||
|
||||
```go
|
||||
type PatchableRole struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
```
|
||||
|
||||
Both are optional. Do not introduce them if `PostableX` already covers the case.
|
||||
|
||||
## What to avoid
|
||||
|
||||
- **Do not mint a flavor that mirrors the core type.** If `StorableX` would have the same fields as `X`, use `X` directly with `bun.BaseModel` embedded. `Channel` is the canonical example.
|
||||
- **Do not bolt domain methods onto `StorableX`.** Storage types are data carriers. Domain methods live on `X`.
|
||||
- **Do not invent new suffixes** (`Creatable`, `Fetchable`, `Savable`). The core type plus `Postable` / `Gettable` / `Updatable` / `Patchable` / `Storable` covers every case that exists today.
|
||||
- **Spelling — `Updatable`, not `Updateable`.** `Updateable` is a common typo. Prefer the shorter form when introducing new types, and rename any stragglers you come across.
|
||||
- **Spelling — `Storable`, not `Storeable`.** `Storeable` is a common typo. Prefer the shorter form when introducing new types, and rename any stragglers you come across.
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- Every domain package defines the core type `X`. Only `X` is mandatory.
|
||||
- Add `PostableX` / `GettableX` / `UpdatableX` / `StorableX` one at a time, only when the shape actually diverges from `X`.
|
||||
- Domain logic lives on `X`, not on the flavor types.
|
||||
- Conversions can be a `New<Output>From<Input>` constructor or a receiver-style `ToY()` method — pick whichever reads best at the call site.
|
||||
- Use a type alias when two shapes are truly identical.
|
||||
- `pkg/types/<domain>/` must not import from other `pkg/` packages.
|
||||
|
||||
## Further reading
|
||||
|
||||
- [abstractions.md](abstractions.md) — when to introduce a new type at all.
|
||||
- [handler.md](handler.md) — struct tag rules at the HTTP boundary.
|
||||
- [packages.md](packages.md) — where types live under `pkg/types/`.
|
||||
- [sql.md](sql.md) — star-schema requirements for `StorableX`.
|
||||
@@ -14,6 +14,7 @@ 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"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
|
||||
type APIHandlerOptions struct {
|
||||
DataConnector interfaces.Reader
|
||||
RulesManager *rules.Manager
|
||||
UsageManager *usage.Manager
|
||||
IntegrationsController *integrations.Controller
|
||||
CloudIntegrationsController *cloudintegrations.Controller
|
||||
@@ -41,6 +43,7 @@ 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,
|
||||
@@ -61,6 +64,10 @@ 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,6 +12,10 @@ 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"
|
||||
|
||||
@@ -19,10 +23,18 @@ 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"
|
||||
@@ -37,6 +49,7 @@ 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"
|
||||
)
|
||||
|
||||
@@ -44,6 +57,7 @@ import (
|
||||
type Server struct {
|
||||
config signoz.Config
|
||||
signoz *signoz.SigNoz
|
||||
ruleManager *baserules.Manager
|
||||
|
||||
// public http router
|
||||
httpConn net.Listener
|
||||
@@ -83,6 +97,24 @@ 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)
|
||||
|
||||
@@ -131,6 +163,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
|
||||
apiOpts := api.APIHandlerOptions{
|
||||
DataConnector: reader,
|
||||
RulesManager: rm,
|
||||
UsageManager: usageManager,
|
||||
IntegrationsController: integrationsController,
|
||||
CloudIntegrationsController: cloudIntegrationsController,
|
||||
@@ -147,7 +180,8 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
|
||||
s := &Server{
|
||||
config: config,
|
||||
signoz: signoz,
|
||||
signoz: signoz,
|
||||
ruleManager: rm,
|
||||
httpHostPort: baseconst.HTTPHostPort,
|
||||
unavailableChannel: make(chan healthcheck.Status),
|
||||
usageManager: usageManager,
|
||||
@@ -228,20 +262,6 @@ 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
|
||||
@@ -268,6 +288,8 @@ 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
|
||||
@@ -311,9 +333,47 @@ 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.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/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/design-tokens": "2.1.4",
|
||||
"@signozhq/dialog": "0.0.4",
|
||||
"@signozhq/drawer": "0.0.6",
|
||||
"@signozhq/dialog": "^0.0.2",
|
||||
"@signozhq/drawer": "0.0.4",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
"@signozhq/input": "0.0.4",
|
||||
"@signozhq/popover": "0.1.2",
|
||||
"@signozhq/radio-group": "0.0.4",
|
||||
"@signozhq/resizable": "0.0.2",
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
"@signozhq/radio-group": "0.0.2",
|
||||
"@signozhq/resizable": "0.0.0",
|
||||
"@signozhq/table": "0.3.7",
|
||||
"@signozhq/toggle-group": "0.0.3",
|
||||
"@signozhq/toggle-group": "0.0.1",
|
||||
"@signozhq/ui": "0.0.5",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
|
||||
@@ -1,496 +0,0 @@
|
||||
/**
|
||||
* ! 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,24 +6,17 @@
|
||||
*/
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType } from '../../../generatedAPIInstance';
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type {
|
||||
CreateRule201,
|
||||
DeleteRuleByIDPathParameters,
|
||||
GetRuleByID200,
|
||||
GetRuleByIDPathParameters,
|
||||
GetRuleHistoryFilterKeys200,
|
||||
GetRuleHistoryFilterKeysParams,
|
||||
GetRuleHistoryFilterKeysPathParameters,
|
||||
@@ -42,548 +35,9 @@ 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
|
||||
@@ -1288,87 +742,3 @@ export const invalidateGetRuleHistoryTopContributors = async (
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint fires a test notification for the given rule definition
|
||||
* @summary Test alert rule
|
||||
*/
|
||||
export const testRule = (
|
||||
ruletypesPostableRuleDTO: BodyType<RuletypesPostableRuleDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<TestRule200>({
|
||||
url: `/api/v2/rules/test`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: ruletypesPostableRuleDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getTestRuleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testRule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testRule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['testRule'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof testRule>>,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return testRule(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type TestRuleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof testRule>>
|
||||
>;
|
||||
export type TestRuleMutationBody = BodyType<RuletypesPostableRuleDTO>;
|
||||
export type TestRuleMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Test alert rule
|
||||
*/
|
||||
export const useTestRule = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testRule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof testRule>>,
|
||||
TError,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getTestRuleMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
|
||||
@@ -4529,20 +4529,6 @@ 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',
|
||||
@@ -4551,513 +4537,6 @@ 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
|
||||
@@ -5977,57 +5456,6 @@ 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
|
||||
@@ -6825,53 +6253,6 @@ 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;
|
||||
};
|
||||
@@ -7142,14 +6523,6 @@ export type GetRuleHistoryTopContributors200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type TestRule200 = {
|
||||
data: RuletypesGettableTestRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetSessionContext200 = {
|
||||
data: AuthtypesSessionContextDTO;
|
||||
/**
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
height: 32px;
|
||||
padding: 10px 16px;
|
||||
background: var(--l2-background);
|
||||
color: var(--l2-foreground);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
@@ -66,3 +65,10 @@
|
||||
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(--secondary-background);
|
||||
color: var(--secondary-foreground);
|
||||
background: var(--card);
|
||||
color: var(--accent-primary);
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -5503,21 +5503,20 @@
|
||||
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.5":
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/button/-/button-0.0.5.tgz#e8220a6e9ed78552694f41700c277956f26232e1"
|
||||
integrity sha512-fgobypuXv2kWGDkqXZoEjcySPHELzI/X515cdcR1hx4N9rizzOglLtYEjGTLR13iQrzLwSsNX8xxsv0/iSCjQg==
|
||||
"@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==
|
||||
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==
|
||||
@@ -5530,28 +5529,14 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.2.3"
|
||||
"@signozhq/button" "workspace:*"
|
||||
"@signozhq/button" "0.0.1"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
date-fns "^4.1.0"
|
||||
@@ -5560,14 +5545,13 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
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"
|
||||
@@ -5575,10 +5559,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@radix-ui/react-checkbox" "^1.2.3"
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
@@ -5589,24 +5573,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-popover" "^1.1.2"
|
||||
@@ -5619,10 +5589,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@radix-ui/react-dialog" "^1.1.11"
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
@@ -5639,25 +5609,24 @@
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/design-tokens/-/design-tokens-2.1.4.tgz#f209da6fbd2ac97ab4434b71472f741009306550"
|
||||
integrity sha512-Ny7/VA5YGFFmZx58jMh7ATFyu7VePaJ4ySmj/DopP1hilmfdxQsKWnpqKaZJWRXrbNkc0gmq3cR7q7Z8nnN7ZQ==
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
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.6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/drawer/-/drawer-0.0.6.tgz#ea280d71af6bc665679c7da26e0703a7bf96aa0e"
|
||||
integrity sha512-xoDHdsVZuj4rHK5gTnM4px7E2rv/6Jgqm81uxs1CfheOQsEAnPuiq3Dpdrdsf6XxCeORwnpcGUR5PEJhnAsjmA==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@radix-ui/react-dialog" "^1.1.11"
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
@@ -5678,24 +5647,23 @@
|
||||
"@commitlint/cli" "^17.6.7"
|
||||
"@commitlint/config-conventional" "^17.6.7"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
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.1.2":
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/popover/-/popover-0.1.2.tgz#73b418be584e8a671ff4189ed47a540ed73bbde6"
|
||||
integrity sha512-6TVMVjWuO7XcKfFMrndcmDdg4JsGvRLe0SV43CRPNV6OkL5uFwUHFJZK2BU3v8S9xMoOf3LsdpfxPuCeK8QKCA==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-popover" "^1.1.15"
|
||||
@@ -5707,10 +5675,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-radio-group" "^1.3.4"
|
||||
@@ -5721,10 +5689,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
@@ -5753,10 +5721,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/prometheus/alertmanager/notify"
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
// Templater expands user-authored title and body templates against a group
|
||||
// of alerts and returns channel-ready strings along with the aggregate data
|
||||
// a caller might reuse (e.g. to render an email layout around the body).
|
||||
type Templater interface {
|
||||
Expand(ctx context.Context, req alertmanagertypes.ExpandRequest, alerts []*types.Alert) (*alertmanagertypes.ExpandResult, error)
|
||||
}
|
||||
|
||||
type templater struct {
|
||||
tmpl *template.Template
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// New returns a Templater bound to the given Prometheus alertmanager
|
||||
// template and logger.
|
||||
func New(tmpl *template.Template, logger *slog.Logger) Templater {
|
||||
return &templater{tmpl: tmpl, logger: logger}
|
||||
}
|
||||
|
||||
func (at *templater) Expand(
|
||||
ctx context.Context,
|
||||
req alertmanagertypes.ExpandRequest,
|
||||
alerts []*types.Alert,
|
||||
) (*alertmanagertypes.ExpandResult, error) {
|
||||
ntd := at.buildNotificationTemplateData(ctx, alerts)
|
||||
missingVars := make(map[string]bool)
|
||||
|
||||
title, titleMissingVars, err := at.expandTitle(req.TitleTemplate, ntd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// if title template results in empty string, use default template
|
||||
// this happens for rules where custom title annotation was not set
|
||||
if title == "" && req.DefaultTitleTemplate != "" {
|
||||
title, err = at.expandDefaultTemplate(ctx, req.DefaultTitleTemplate, alerts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
mergeMissingVars(missingVars, titleMissingVars)
|
||||
}
|
||||
|
||||
isDefaultBody := false
|
||||
body, bodyMissingVars, err := at.expandBody(req.BodyTemplate, ntd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// if body template results in nil, use default template
|
||||
// this happens for rules where custom body annotation was not set
|
||||
if body == nil {
|
||||
isDefaultBody = true
|
||||
defaultBody, err := at.expandDefaultTemplate(ctx, req.DefaultBodyTemplate, alerts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = []string{defaultBody} // default template combines all alerts message into a single body
|
||||
} else {
|
||||
mergeMissingVars(missingVars, bodyMissingVars)
|
||||
}
|
||||
|
||||
// convert the internal map to a sorted slice for returning missing variables
|
||||
missingVarsList := make([]string, 0, len(missingVars))
|
||||
for k := range missingVars {
|
||||
missingVarsList = append(missingVarsList, k)
|
||||
}
|
||||
sort.Strings(missingVarsList)
|
||||
|
||||
return &alertmanagertypes.ExpandResult{
|
||||
Title: title,
|
||||
Body: body,
|
||||
MissingVars: missingVarsList,
|
||||
IsDefaultBody: isDefaultBody,
|
||||
NotificationData: ntd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// expandDefaultTemplate uses go-template to expand the default template.
|
||||
func (at *templater) expandDefaultTemplate(
|
||||
ctx context.Context,
|
||||
tmplStr string,
|
||||
alerts []*types.Alert,
|
||||
) (string, error) {
|
||||
// if even the default template is empty, return empty string
|
||||
// this is possible if user added channel with blank template
|
||||
if tmplStr == "" {
|
||||
at.logger.WarnContext(ctx, "default template is empty")
|
||||
return "", nil
|
||||
}
|
||||
data := notify.GetTemplateData(ctx, at.tmpl, alerts, at.logger)
|
||||
result, err := at.tmpl.ExecuteTextString(tmplStr, data)
|
||||
if err != nil {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to execute default template: %s", err.Error())
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// mergeMissingVars adds all keys from src into dst.
|
||||
func mergeMissingVars(dst, src map[string]bool) {
|
||||
for k := range src {
|
||||
dst[k] = true
|
||||
}
|
||||
}
|
||||
|
||||
// expandTitle expands the title template. Returns empty string if the template is empty.
|
||||
func (at *templater) expandTitle(
|
||||
titleTemplate string,
|
||||
ntd *alertmanagertypes.NotificationTemplateData,
|
||||
) (string, map[string]bool, error) {
|
||||
if titleTemplate == "" {
|
||||
return "", nil, nil
|
||||
}
|
||||
processRes, err := preProcessTemplateAndData(titleTemplate, ntd)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
result, err := at.tmpl.ExecuteTextString(processRes.Template, processRes.Data)
|
||||
if err != nil {
|
||||
return "", nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to execute custom title template: %s", err.Error())
|
||||
}
|
||||
return strings.TrimSpace(result), processRes.UnknownVars, nil
|
||||
}
|
||||
|
||||
// expandBody expands the body template for each individual alert. Returns nil if the template is empty.
|
||||
func (at *templater) expandBody(
|
||||
bodyTemplate string,
|
||||
ntd *alertmanagertypes.NotificationTemplateData,
|
||||
) ([]string, map[string]bool, error) {
|
||||
if bodyTemplate == "" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
var sb []string
|
||||
missingVars := make(map[string]bool)
|
||||
for i := range ntd.Alerts {
|
||||
processRes, err := preProcessTemplateAndData(bodyTemplate, &ntd.Alerts[i])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
part, err := at.tmpl.ExecuteTextString(processRes.Template, processRes.Data)
|
||||
if err != nil {
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to execute custom body template: %s", err.Error())
|
||||
}
|
||||
// add unknown variables and templated text to the result
|
||||
for k := range processRes.UnknownVars {
|
||||
missingVars[k] = true
|
||||
}
|
||||
if strings.TrimSpace(part) != "" {
|
||||
sb = append(sb, strings.TrimSpace(part))
|
||||
}
|
||||
}
|
||||
return sb, missingVars, nil
|
||||
}
|
||||
|
||||
// buildNotificationTemplateData creates the NotificationTemplateData using
|
||||
// info from context and the raw alerts.
|
||||
func (at *templater) buildNotificationTemplateData(
|
||||
ctx context.Context,
|
||||
alerts []*types.Alert,
|
||||
) *alertmanagertypes.NotificationTemplateData {
|
||||
// extract the required data from the context
|
||||
receiver, ok := notify.ReceiverName(ctx)
|
||||
if !ok {
|
||||
at.logger.WarnContext(ctx, "missing receiver name in context")
|
||||
}
|
||||
|
||||
groupLabels, ok := notify.GroupLabels(ctx)
|
||||
if !ok {
|
||||
at.logger.WarnContext(ctx, "missing group labels in context")
|
||||
}
|
||||
|
||||
// extract the external URL from the template
|
||||
externalURL := ""
|
||||
if at.tmpl.ExternalURL != nil {
|
||||
externalURL = at.tmpl.ExternalURL.String()
|
||||
}
|
||||
|
||||
commonAnnotations := extractCommonKV(alerts, func(a *types.Alert) model.LabelSet { return a.Annotations })
|
||||
commonLabels := extractCommonKV(alerts, func(a *types.Alert) model.LabelSet { return a.Labels })
|
||||
|
||||
// aggregate labels and annotations from all alerts
|
||||
labels := aggregateKV(alerts, func(a *types.Alert) model.LabelSet { return a.Labels })
|
||||
annotations := aggregateKV(alerts, func(a *types.Alert) model.LabelSet { return a.Annotations })
|
||||
|
||||
// Strip private annotations from surfaces visible to templates or
|
||||
// notifications; the structured fields on AlertInfo/RuleInfo already hold
|
||||
// anything a template needs from them.
|
||||
commonAnnotations = alertmanagertypes.FilterPublicAnnotations(commonAnnotations)
|
||||
annotations = alertmanagertypes.FilterPublicAnnotations(annotations)
|
||||
|
||||
// build the alert data slice
|
||||
alertDataSlice := make([]alertmanagertypes.AlertData, 0, len(alerts))
|
||||
for _, a := range alerts {
|
||||
ad := buildAlertData(a, receiver)
|
||||
alertDataSlice = append(alertDataSlice, ad)
|
||||
}
|
||||
|
||||
// count the number of firing and resolved alerts
|
||||
var firing, resolved int
|
||||
for _, ad := range alertDataSlice {
|
||||
if ad.Alert.IsFiring {
|
||||
firing++
|
||||
} else if ad.Alert.IsResolved {
|
||||
resolved++
|
||||
}
|
||||
}
|
||||
|
||||
// build the group labels
|
||||
gl := make(template.KV, len(groupLabels))
|
||||
for k, v := range groupLabels {
|
||||
gl[string(k)] = string(v)
|
||||
}
|
||||
|
||||
// build the notification template data
|
||||
return &alertmanagertypes.NotificationTemplateData{
|
||||
Alert: alertmanagertypes.NotificationAlert{
|
||||
Receiver: receiver,
|
||||
Status: string(types.Alerts(alerts...).Status()),
|
||||
TotalFiring: firing,
|
||||
TotalResolved: resolved,
|
||||
},
|
||||
Rule: buildRuleInfo(commonLabels, commonAnnotations),
|
||||
GroupLabels: gl,
|
||||
CommonLabels: commonLabels,
|
||||
CommonAnnotations: commonAnnotations,
|
||||
ExternalURL: externalURL,
|
||||
Labels: labels,
|
||||
Annotations: annotations,
|
||||
Alerts: alertDataSlice,
|
||||
}
|
||||
}
|
||||
|
||||
// buildAlertData converts a single *types.Alert into an AlertData.
|
||||
func buildAlertData(a *types.Alert, receiver string) alertmanagertypes.AlertData {
|
||||
labels := make(template.KV, len(a.Labels))
|
||||
for k, v := range a.Labels {
|
||||
labels[string(k)] = string(v)
|
||||
}
|
||||
|
||||
annotations := make(template.KV, len(a.Annotations))
|
||||
for k, v := range a.Annotations {
|
||||
annotations[string(k)] = string(v)
|
||||
}
|
||||
|
||||
return alertmanagertypes.AlertData{
|
||||
Alert: alertmanagertypes.AlertInfo{
|
||||
Status: string(a.Status()),
|
||||
Receiver: receiver,
|
||||
Value: annotations[ruletypes.AnnotationValue],
|
||||
StartsAt: a.StartsAt,
|
||||
EndsAt: a.EndsAt,
|
||||
GeneratorURL: a.GeneratorURL,
|
||||
Fingerprint: a.Fingerprint().String(),
|
||||
IsFiring: a.Status() == model.AlertFiring,
|
||||
IsResolved: a.Status() == model.AlertResolved,
|
||||
IsMissingData: labels[ruletypes.LabelNoData] == "true",
|
||||
IsRecovering: labels[ruletypes.LabelIsRecovering] == "true",
|
||||
},
|
||||
Rule: buildRuleInfo(labels, annotations),
|
||||
Log: alertmanagertypes.LinkInfo{URL: annotations[ruletypes.AnnotationRelatedLogs]},
|
||||
Trace: alertmanagertypes.LinkInfo{URL: annotations[ruletypes.AnnotationRelatedTraces]},
|
||||
Labels: labels,
|
||||
// Strip private annotations once the structured fields above have
|
||||
// been populated from the raw map.
|
||||
Annotations: alertmanagertypes.FilterPublicAnnotations(annotations),
|
||||
}
|
||||
}
|
||||
|
||||
// buildRuleInfo extracts the rule metadata from the well-known labels and
|
||||
// annotations that the rule manager attaches to every emitted alert.
|
||||
func buildRuleInfo(labels, annotations template.KV) alertmanagertypes.RuleInfo {
|
||||
return alertmanagertypes.RuleInfo{
|
||||
Name: labels[ruletypes.LabelAlertName],
|
||||
ID: labels[ruletypes.LabelRuleID],
|
||||
URL: labels[ruletypes.LabelRuleSource],
|
||||
Severity: labels[ruletypes.LabelSeverityName],
|
||||
MatchType: annotations[ruletypes.AnnotationMatchType],
|
||||
Threshold: alertmanagertypes.Threshold{
|
||||
Value: annotations[ruletypes.AnnotationThresholdValue],
|
||||
Op: annotations[ruletypes.AnnotationCompareOp],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// maxAggregatedValues caps the number of distinct label/annotation values
|
||||
// joined together when summarising across alerts. Beyond this, extras are
|
||||
// dropped rather than concatenated.
|
||||
const maxAggregatedValues = 5
|
||||
|
||||
// aggregateKV merges label or annotation sets from a group of alerts into a
|
||||
// single KV. Per key, up to maxAggregatedValues distinct values are kept (in
|
||||
// order of first appearance) and joined with ", ". A lossy summary used for
|
||||
// grouped-notification display, not a true union.
|
||||
func aggregateKV(alerts []*types.Alert, extractFn func(*types.Alert) model.LabelSet) template.KV {
|
||||
valuesPerKey := make(map[string][]string)
|
||||
seenValues := make(map[string]map[string]bool)
|
||||
|
||||
for _, alert := range alerts {
|
||||
for k, v := range extractFn(alert) {
|
||||
key := string(k)
|
||||
value := string(v)
|
||||
|
||||
if seenValues[key] == nil {
|
||||
seenValues[key] = make(map[string]bool)
|
||||
}
|
||||
if !seenValues[key][value] && len(valuesPerKey[key]) < maxAggregatedValues {
|
||||
seenValues[key][value] = true
|
||||
valuesPerKey[key] = append(valuesPerKey[key], value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(template.KV, len(valuesPerKey))
|
||||
for key, values := range valuesPerKey {
|
||||
result[key] = strings.Join(values, ", ")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// extractCommonKV returns the intersection of label or annotation pairs
|
||||
// across all alerts. A pair is included only if every alert carries the same
|
||||
// key with the same value.
|
||||
func extractCommonKV(alerts []*types.Alert, extractFn func(*types.Alert) model.LabelSet) template.KV {
|
||||
if len(alerts) == 0 {
|
||||
return template.KV{}
|
||||
}
|
||||
|
||||
common := make(template.KV, len(extractFn(alerts[0])))
|
||||
for k, v := range extractFn(alerts[0]) {
|
||||
common[string(k)] = string(v)
|
||||
}
|
||||
|
||||
for _, a := range alerts[1:] {
|
||||
kv := extractFn(a)
|
||||
for k := range common {
|
||||
if string(kv[model.LabelName(k)]) != common[k] {
|
||||
delete(common, k)
|
||||
}
|
||||
}
|
||||
if len(common) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return common
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
test "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/alertmanagernotifytest"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/prometheus/alertmanager/notify"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
)
|
||||
|
||||
// testSetup returns an AlertTemplater and a context pre-populated with group key,
|
||||
// receiver name, and group labels for use in tests.
|
||||
func testSetup(t *testing.T) (Templater, context.Context) {
|
||||
t.Helper()
|
||||
tmpl := test.CreateTmpl(t)
|
||||
ctx := context.Background()
|
||||
ctx = notify.WithGroupKey(ctx, "test-group")
|
||||
ctx = notify.WithReceiverName(ctx, "slack")
|
||||
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": "TestAlert", "severity": "critical"})
|
||||
logger := slog.New(slog.DiscardHandler)
|
||||
return New(tmpl, logger), ctx
|
||||
}
|
||||
|
||||
func createAlert(labels, annotations map[string]string, isFiring bool) *types.Alert {
|
||||
ls := model.LabelSet{}
|
||||
for k, v := range labels {
|
||||
ls[model.LabelName(k)] = model.LabelValue(v)
|
||||
}
|
||||
ann := model.LabelSet{}
|
||||
for k, v := range annotations {
|
||||
ann[model.LabelName(k)] = model.LabelValue(v)
|
||||
}
|
||||
startsAt := time.Now()
|
||||
var endsAt time.Time
|
||||
if isFiring {
|
||||
endsAt = startsAt.Add(time.Hour)
|
||||
} else {
|
||||
startsAt = startsAt.Add(-2 * time.Hour)
|
||||
endsAt = startsAt.Add(-time.Hour)
|
||||
}
|
||||
return &types.Alert{Alert: model.Alert{Labels: ls, Annotations: ann, StartsAt: startsAt, EndsAt: endsAt}}
|
||||
}
|
||||
|
||||
func TestExpandTemplates(t *testing.T) {
|
||||
at, ctx := testSetup(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
alerts []*types.Alert
|
||||
input alertmanagertypes.ExpandRequest
|
||||
wantTitle string
|
||||
wantBody []string
|
||||
wantMissingVars []string
|
||||
errorContains string
|
||||
wantIsDefaultBody bool
|
||||
}{
|
||||
{
|
||||
// High request throughput on a service — service is a custom label.
|
||||
// $labels.service extracts the label value; $annotations.description pulls the annotation.
|
||||
name: "new template: high request throughput for a service",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(
|
||||
map[string]string{
|
||||
ruletypes.LabelAlertName: "HighRequestThroughput",
|
||||
ruletypes.LabelSeverityName: "warning",
|
||||
"service.name": "payment-service",
|
||||
},
|
||||
map[string]string{"description": "Request rate exceeded 10k/s"},
|
||||
true,
|
||||
),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: "High request throughput for $service.name",
|
||||
BodyTemplate: `The service $service.name is getting high request. Please investigate.
|
||||
Severity: $rule.severity
|
||||
Status: $alert.status
|
||||
Service: $service.name
|
||||
Description: $description`,
|
||||
},
|
||||
wantTitle: "High request throughput for payment-service",
|
||||
wantBody: []string{`The service payment-service is getting high request. Please investigate.
|
||||
Severity: warning
|
||||
Status: firing
|
||||
Service: payment-service
|
||||
Description: Request rate exceeded 10k/s`},
|
||||
wantIsDefaultBody: false,
|
||||
},
|
||||
{
|
||||
// Disk usage alert using old Go template syntax throughout.
|
||||
// No custom templates — both title and body use the default fallback path.
|
||||
name: "old template: disk usage high on database host",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(
|
||||
map[string]string{ruletypes.LabelAlertName: "DiskUsageHigh",
|
||||
ruletypes.LabelSeverityName: "critical",
|
||||
"instance": "db-primary-01",
|
||||
},
|
||||
map[string]string{
|
||||
"summary": "Disk usage high on database host",
|
||||
"description": "Disk usage is high on the database host",
|
||||
"related_logs": "https://logs.example.com/search?q=DiskUsageHigh",
|
||||
"related_traces": "https://traces.example.com/search?q=DiskUsageHigh",
|
||||
},
|
||||
true,
|
||||
),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
DefaultTitleTemplate: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
|
||||
{{- if gt (len .CommonLabels) (len .GroupLabels) -}}
|
||||
{{" "}}(
|
||||
{{- with .CommonLabels.Remove .GroupLabels.Names }}
|
||||
{{- range $index, $label := .SortedPairs -}}
|
||||
{{ if $index }}, {{ end }}
|
||||
{{- $label.Name }}="{{ $label.Value -}}"
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
)
|
||||
{{- end }}`,
|
||||
DefaultBodyTemplate: `{{ range .Alerts -}}
|
||||
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
|
||||
|
||||
*Summary:* {{ .Annotations.summary }}
|
||||
*Description:* {{ .Annotations.description }}
|
||||
*RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}}
|
||||
*RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end}}
|
||||
|
||||
*Details:*
|
||||
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
|
||||
{{ end }}
|
||||
{{ end }}`,
|
||||
},
|
||||
wantTitle: "[FIRING:1] DiskUsageHigh for (instance=\"db-primary-01\")",
|
||||
// Written with explicit \n so trailing whitespace inside the body
|
||||
// (emitted by the un-trimmed "{{ end }}" in the default template)
|
||||
// survives format-on-save.
|
||||
wantBody: []string{"*Alert:* DiskUsageHigh - critical\n" +
|
||||
"\n" +
|
||||
" *Summary:* Disk usage high on database host\n" +
|
||||
" *Description:* Disk usage is high on the database host\n" +
|
||||
" *RelatedLogs:* View in <https://logs.example.com/search?q=DiskUsageHigh|logs explorer>\n" +
|
||||
" *RelatedTraces:* View in <https://traces.example.com/search?q=DiskUsageHigh|traces explorer>\n" +
|
||||
"\n" +
|
||||
" *Details:*\n" +
|
||||
" • *alertname:* DiskUsageHigh\n" +
|
||||
" • *instance:* db-primary-01\n" +
|
||||
" • *severity:* critical\n" +
|
||||
" \n" +
|
||||
" "},
|
||||
wantIsDefaultBody: true,
|
||||
},
|
||||
{
|
||||
// Pod crash loop on multiple pods — body is expanded once per alert
|
||||
// and joined with "\n\n", with the pod name pulled from labels.
|
||||
name: "new template: pod crash loop on multiple pods, body per-alert",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrashLoop", "pod": "api-worker-1"}, nil, true),
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrashLoop", "pod": "api-worker-2"}, nil, true),
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrashLoop", "pod": "api-worker-3"}, nil, true),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: "$rule.name: $alert.total_firing pods affected",
|
||||
BodyTemplate: "$labels.pod is crash looping",
|
||||
},
|
||||
wantTitle: "PodCrashLoop: 3 pods affected",
|
||||
wantBody: []string{"api-worker-1 is crash looping", "api-worker-2 is crash looping", "api-worker-3 is crash looping"},
|
||||
wantIsDefaultBody: false,
|
||||
},
|
||||
{
|
||||
// Incident partially resolved — one service still down, one recovered.
|
||||
// Title shows the aggregate counts; body shows per-service status.
|
||||
name: "new template: service degradation with mixed firing and resolved alerts",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "ServiceDown", "service": "auth-service"}, nil, true),
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "ServiceDown", "service": "payment-service"}, nil, false),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: "$alert.total_firing firing, $alert.total_resolved resolved",
|
||||
BodyTemplate: "$labels.service ($alert.status)",
|
||||
},
|
||||
wantTitle: "1 firing, 1 resolved",
|
||||
wantBody: []string{"auth-service (firing)", "payment-service (resolved)"},
|
||||
wantIsDefaultBody: false,
|
||||
},
|
||||
{
|
||||
// $environment is not a known AlertData or NotificationTemplateData field,
|
||||
// so it lands in MissingVars and renders as "<no value>" in the output.
|
||||
name: "missing vars: unknown $environment variable in title",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical"}, nil, true),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: "[$environment] $rule.name",
|
||||
},
|
||||
wantTitle: "[<no value>] HighCPU",
|
||||
wantMissingVars: []string{"environment"},
|
||||
wantIsDefaultBody: true,
|
||||
},
|
||||
{
|
||||
// $runbook_url is not a known field — someone tried to embed a runbook link
|
||||
// directly as a variable instead of via annotations.
|
||||
name: "missing vars: unknown $runbook_url variable in body",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "PodOOMKilled", ruletypes.LabelSeverityName: "warning"}, nil, true),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
BodyTemplate: "$rule.name: see runbook at $runbook_url",
|
||||
},
|
||||
wantBody: []string{"PodOOMKilled: see runbook at <no value>"},
|
||||
wantMissingVars: []string{"runbook_url"},
|
||||
},
|
||||
{
|
||||
// Both title and body use unknown variables; MissingVars is the union of both.
|
||||
name: "missing vars: unknown variables in both title and body",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "HighMemory", ruletypes.LabelSeverityName: "critical"}, nil, true),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: "[$environment] $rule.name and [{{ $service }}]",
|
||||
BodyTemplate: "$rule.name: see runbook at $runbook_url",
|
||||
},
|
||||
wantTitle: "[<no value>] HighMemory and [<no value>]",
|
||||
wantBody: []string{"HighMemory: see runbook at <no value>"},
|
||||
wantMissingVars: []string{"environment", "runbook_url", "service"},
|
||||
},
|
||||
{
|
||||
// Custom title template that expands to only whitespace triggers the fallback,
|
||||
// so the default title template is used instead.
|
||||
name: "fallback: whitespace-only custom title falls back to default",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical"}, nil, false),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: " ",
|
||||
DefaultTitleTemplate: "{{ .CommonLabels.alertname }} ({{ .Status | toUpper }})",
|
||||
DefaultBodyTemplate: "Runbook: https://runbook.example.com",
|
||||
},
|
||||
wantTitle: "HighCPU (RESOLVED)",
|
||||
wantBody: []string{"Runbook: https://runbook.example.com"},
|
||||
wantIsDefaultBody: true,
|
||||
},
|
||||
{
|
||||
name: "using non-existing function in template",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(map[string]string{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical"}, nil, true),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
TitleTemplate: "$rule.name ({{$severity | toUpperAndTrim}}) for $alertname",
|
||||
},
|
||||
errorContains: "function \"toUpperAndTrim\" not defined",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := at.Expand(ctx, tc.input, tc.alerts)
|
||||
if tc.errorContains != "" {
|
||||
require.ErrorContains(t, err, tc.errorContains)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
if tc.wantTitle != "" {
|
||||
require.Equal(t, tc.wantTitle, got.Title)
|
||||
}
|
||||
if tc.wantBody != nil {
|
||||
require.Equal(t, tc.wantBody, got.Body)
|
||||
}
|
||||
require.Equal(t, tc.wantIsDefaultBody, got.IsDefaultBody)
|
||||
|
||||
if len(tc.wantMissingVars) == 0 {
|
||||
require.Empty(t, got.MissingVars)
|
||||
} else {
|
||||
sort.Strings(tc.wantMissingVars)
|
||||
require.Equal(t, tc.wantMissingVars, got.MissingVars)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
)
|
||||
|
||||
// ExtractTemplatesFromAnnotations pulls the user-authored title and body
|
||||
// templates off the well-known annotation keys attached by the rule manager.
|
||||
// A template is returned only if every alert in the group carries the same
|
||||
// value under that key; otherwise the empty string is returned for that slot
|
||||
// (which causes Expand to fall back to the channel default).
|
||||
func ExtractTemplatesFromAnnotations(alerts []*types.Alert) (titleTemplate, bodyTemplate string) {
|
||||
if len(alerts) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
title := string(alerts[0].Annotations[ruletypes.AnnotationTitleTemplate])
|
||||
body := string(alerts[0].Annotations[ruletypes.AnnotationBodyTemplate])
|
||||
|
||||
for _, a := range alerts[1:] {
|
||||
if title != "" && string(a.Annotations[ruletypes.AnnotationTitleTemplate]) != title {
|
||||
title = ""
|
||||
}
|
||||
if body != "" && string(a.Annotations[ruletypes.AnnotationBodyTemplate]) != body {
|
||||
body = ""
|
||||
}
|
||||
if title == "" && body == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return title, body
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAggregateKV(t *testing.T) {
|
||||
extractLabels := func(a *types.Alert) model.LabelSet { return a.Labels }
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
alerts []*types.Alert
|
||||
extractFn func(*types.Alert) model.LabelSet
|
||||
expected template.KV
|
||||
}{
|
||||
{
|
||||
name: "empty alerts slice",
|
||||
alerts: []*types.Alert{},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{},
|
||||
},
|
||||
{
|
||||
name: "single alert",
|
||||
alerts: []*types.Alert{
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{
|
||||
"env": "production",
|
||||
"service": "backend",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{
|
||||
"env": "production",
|
||||
"service": "backend",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "varying values with duplicates deduped",
|
||||
alerts: []*types.Alert{
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "production", "service": "backend"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "production", "service": "api"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "production", "service": "frontend"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "production", "service": "api"}}},
|
||||
},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{
|
||||
"env": "production",
|
||||
"service": "backend, api, frontend",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "more than 5 unique values truncates to 5",
|
||||
alerts: []*types.Alert{
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc1"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc2"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc3"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc4"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc5"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc6"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc7"}}},
|
||||
},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{
|
||||
"service": "svc1, svc2, svc3, svc4, svc5",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := aggregateKV(tc.alerts, tc.extractFn)
|
||||
require.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractCommonKV(t *testing.T) {
|
||||
extractLabels := func(a *types.Alert) model.LabelSet { return a.Labels }
|
||||
extractAnnotations := func(a *types.Alert) model.LabelSet { return a.Annotations }
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
alerts []*types.Alert
|
||||
extractFn func(*types.Alert) model.LabelSet
|
||||
expected template.KV
|
||||
}{
|
||||
{
|
||||
name: "empty alerts slice",
|
||||
alerts: []*types.Alert{},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{},
|
||||
},
|
||||
{
|
||||
name: "single alert returns all labels",
|
||||
alerts: []*types.Alert{
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "service": "api"}}},
|
||||
},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{"env": "prod", "service": "api"},
|
||||
},
|
||||
{
|
||||
name: "multiple alerts with fully common labels",
|
||||
alerts: []*types.Alert{
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "region": "us-east"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "region": "us-east"}}},
|
||||
},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{"env": "prod", "region": "us-east"},
|
||||
},
|
||||
{
|
||||
name: "multiple alerts with partially common labels",
|
||||
alerts: []*types.Alert{
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "service": "api"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "service": "worker"}}},
|
||||
},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{"env": "prod"},
|
||||
},
|
||||
{
|
||||
name: "multiple alerts with no common labels",
|
||||
alerts: []*types.Alert{
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "api"}}},
|
||||
{Alert: model.Alert{Labels: model.LabelSet{"service": "worker"}}},
|
||||
},
|
||||
extractFn: extractLabels,
|
||||
expected: template.KV{},
|
||||
},
|
||||
{
|
||||
name: "annotations extract common annotations",
|
||||
alerts: []*types.Alert{
|
||||
{Alert: model.Alert{Annotations: model.LabelSet{"summary": "high cpu", "runbook": "http://x"}}},
|
||||
{Alert: model.Alert{Annotations: model.LabelSet{"summary": "high cpu", "runbook": "http://y"}}},
|
||||
},
|
||||
extractFn: extractAnnotations,
|
||||
expected: template.KV{"summary": "high cpu"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := extractCommonKV(tc.alerts, tc.extractFn)
|
||||
require.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
)
|
||||
|
||||
// fieldPath is a dotted mapstructure path into the templating data map,
|
||||
// e.g. "alert.is_firing" or "rule.threshold.value".
|
||||
type fieldPath string
|
||||
|
||||
// extractFieldMappings flattens the struct hierarchy into a list of dotted
|
||||
// mapstructure paths that user templates can reference. It emits:
|
||||
// - a leaf for every scalar field
|
||||
// - a leaf for every map field (labels, annotations)
|
||||
// - a mapping for each intermediate sub-struct itself, so {{ $alert := .alert }}
|
||||
// bindings let action blocks write {{ if $alert.is_firing }}
|
||||
//
|
||||
// Slices and interfaces are not surfaced. Pointer fields are dereferenced.
|
||||
func extractFieldMappings(data any) []fieldPath {
|
||||
val := reflect.ValueOf(data)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
if val.IsNil() {
|
||||
return nil
|
||||
}
|
||||
val = val.Elem()
|
||||
}
|
||||
if val.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
return collectFieldMappings(val, "")
|
||||
}
|
||||
|
||||
func collectFieldMappings(val reflect.Value, prefix string) []fieldPath {
|
||||
typ := val.Type()
|
||||
var paths []fieldPath
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
tag := field.Tag.Get("mapstructure")
|
||||
if tag == "" || tag == "-" {
|
||||
continue
|
||||
}
|
||||
name := strings.Split(tag, ",")[0]
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
key := name
|
||||
if prefix != "" {
|
||||
key = prefix + "." + name
|
||||
}
|
||||
|
||||
ft := field.Type
|
||||
if ft.Kind() == reflect.Ptr {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
|
||||
switch ft.Kind() {
|
||||
case reflect.Slice, reflect.Interface:
|
||||
continue
|
||||
}
|
||||
|
||||
// Recurse into sub-structs (time.Time treated as a leaf).
|
||||
if ft.Kind() == reflect.Struct && ft.String() != "time.Time" {
|
||||
paths = append(paths, fieldPath(key))
|
||||
fv := val.Field(i)
|
||||
if fv.Kind() == reflect.Ptr {
|
||||
if fv.IsNil() {
|
||||
continue
|
||||
}
|
||||
fv = fv.Elem()
|
||||
}
|
||||
paths = append(paths, collectFieldMappings(fv, key)...)
|
||||
continue
|
||||
}
|
||||
|
||||
paths = append(paths, fieldPath(key))
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// structRootSet returns the top-level mapstructure tag names whose field
|
||||
// type is a nested struct (excluding time.Time and map/slice/interface
|
||||
// fields). These are the paths the rewriter walks segment-by-segment; any
|
||||
// other dotted $-reference is treated as a flat key on the root map so that
|
||||
// flattened OTel-style label keys like "service.name" resolve naturally.
|
||||
func structRootSet(data any) map[string]bool {
|
||||
val := reflect.ValueOf(data)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
if val.IsNil() {
|
||||
return nil
|
||||
}
|
||||
val = val.Elem()
|
||||
}
|
||||
if val.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
roots := make(map[string]bool)
|
||||
typ := val.Type()
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
tag := field.Tag.Get("mapstructure")
|
||||
if tag == "" || tag == "-" {
|
||||
continue
|
||||
}
|
||||
name := strings.Split(tag, ",")[0]
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
ft := field.Type
|
||||
if ft.Kind() == reflect.Ptr {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
if ft.Kind() == reflect.Struct && ft.String() != "time.Time" {
|
||||
roots[name] = true
|
||||
}
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
// buildDataMap converts the typed data struct to the map[string]any that the
|
||||
// template engine indexes into. Each label and annotation is additionally
|
||||
// exposed at the root under its raw key, so $service.name resolves a flat
|
||||
// OTel-style label as a single-key index on the root. Struct-path keys
|
||||
// already present at the root take precedence on collisions.
|
||||
func buildDataMap(data any) (map[string]any, error) {
|
||||
var result map[string]any
|
||||
if err := mapstructure.Decode(data, &result); err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to build template data map")
|
||||
}
|
||||
|
||||
flatten := func(labels, annotations map[string]string) {
|
||||
for k, v := range labels {
|
||||
if _, ok := result[k]; !ok {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
for k, v := range annotations {
|
||||
if _, ok := result[k]; !ok {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch data := data.(type) {
|
||||
case *alertmanagertypes.NotificationTemplateData:
|
||||
flatten(data.Labels, data.Annotations)
|
||||
case *alertmanagertypes.AlertData:
|
||||
flatten(data.Labels, data.Annotations)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// renderPreamble serialises a map of binding name → RHS expression into
|
||||
// `{{ $name := expr }}` declarations. Dotted names are skipped: Go's
|
||||
// text/template parser rejects `{{ $a.b := ... }}`; dotted paths are resolved
|
||||
// at expansion time by the rewriter.
|
||||
func renderPreamble(bindings map[string]string) string {
|
||||
if len(bindings) == 0 {
|
||||
return ""
|
||||
}
|
||||
var sb strings.Builder
|
||||
for name, expr := range bindings {
|
||||
if strings.Contains(name, ".") {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&sb, `{{ $%s := %s }}`, name, expr)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// buildPreamble constructs the variable-definition preamble prepended to the
|
||||
// user template, covering:
|
||||
// - known root-level struct paths ({{ $alert := .alert }})
|
||||
// - "<no value>" stubs for $-refs whose first segment matches nothing, so
|
||||
// action blocks like {{ if $custom_note }} don't error at parse time
|
||||
//
|
||||
// The set of unmatched names is returned separately so callers (preview API)
|
||||
// can surface warnings.
|
||||
func buildPreamble(tmpl string, data any) (string, map[string]bool, error) {
|
||||
bindings := make(map[string]string)
|
||||
// knownFirstSegments tracks every valid first segment of a $-ref, since
|
||||
// extractUsedVariables only gives us first segments. A label key like
|
||||
// "service.name" contributes "service" here, so $service.name isn't
|
||||
// flagged as unknown even though "service" has no direct binding.
|
||||
knownFirstSegments := make(map[string]bool)
|
||||
|
||||
for _, p := range extractFieldMappings(data) {
|
||||
bindings[string(p)] = fmt.Sprintf(".%s", p)
|
||||
knownFirstSegments[firstSegment(string(p))] = true
|
||||
}
|
||||
// Labels/annotations are flattened into the root map by buildDataMap, so
|
||||
// a bare-accessible key (no dots) can be bound in the preamble — this is
|
||||
// what makes {{ if $severity }} or {{ $severity | toUpper }} work in
|
||||
// action blocks. Dotted label keys only contribute to knownFirstSegments:
|
||||
// their action-block use would be a syntax error anyway ($a.b is not a
|
||||
// valid Go template identifier).
|
||||
for k := range dataLabelsAndAnnotations(data) {
|
||||
knownFirstSegments[firstSegment(k)] = true
|
||||
if !strings.Contains(k, ".") {
|
||||
if _, ok := bindings[k]; !ok {
|
||||
bindings[k] = fmt.Sprintf(".%s", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
used, err := extractUsedVariables(tmpl)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
unknown := make(map[string]bool)
|
||||
for name := range used {
|
||||
if !knownFirstSegments[name] {
|
||||
unknown[name] = true
|
||||
bindings[name] = `"<no value>"`
|
||||
}
|
||||
}
|
||||
|
||||
return renderPreamble(bindings), unknown, nil
|
||||
}
|
||||
|
||||
// firstSegment returns the portion of a dotted path before the first dot,
|
||||
// or the whole string if there is no dot.
|
||||
func firstSegment(path string) string {
|
||||
if i := strings.IndexByte(path, '.'); i >= 0 {
|
||||
return path[:i]
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// dataLabelsAndAnnotations returns the union of label and annotation keys on
|
||||
// the given data struct (if it carries them). Used for first-segment
|
||||
// recognition of $-refs that point into flat OTel-style label keys.
|
||||
func dataLabelsAndAnnotations(data any) map[string]struct{} {
|
||||
keys := make(map[string]struct{})
|
||||
switch d := data.(type) {
|
||||
case *alertmanagertypes.NotificationTemplateData:
|
||||
for k := range d.Labels {
|
||||
keys[k] = struct{}{}
|
||||
}
|
||||
for k := range d.Annotations {
|
||||
keys[k] = struct{}{}
|
||||
}
|
||||
case *alertmanagertypes.AlertData:
|
||||
for k := range d.Labels {
|
||||
keys[k] = struct{}{}
|
||||
}
|
||||
for k := range d.Annotations {
|
||||
keys[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// processingResult is the rewritten template and its backing data map,
|
||||
// ready to be passed to Go text/template's Execute.
|
||||
type processingResult struct {
|
||||
// Template is the input template with $-refs rewritten to Go template
|
||||
// syntax and an identifier preamble prepended.
|
||||
Template string
|
||||
// Data is the flattened map the template indexes into.
|
||||
Data map[string]any
|
||||
// UnknownVars are $-refs in the input that had no matching data path.
|
||||
// They render as "<no value>" at execution; useful for preview warnings.
|
||||
UnknownVars map[string]bool
|
||||
}
|
||||
|
||||
// preProcessTemplateAndData prepares a user-authored template and its backing
|
||||
// struct for Go text/template execution.
|
||||
//
|
||||
// Input (with data *AlertData):
|
||||
// "$rule.name fired with value $alert.value"
|
||||
// Output:
|
||||
// "{{ $alert := .alert }}{{ $rule := .rule }}..."
|
||||
// "{{ index . \"rule\" \"name\" }} fired with value {{ index . \"alert\" \"value\" }}"
|
||||
func preProcessTemplateAndData(tmpl string, data any) (*processingResult, error) {
|
||||
unknownVars := make(map[string]bool)
|
||||
if tmpl == "" {
|
||||
result, err := buildDataMap(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &processingResult{Data: result, UnknownVars: unknownVars}, nil
|
||||
}
|
||||
|
||||
preamble, unknownVars, err := buildPreamble(tmpl, data)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to build template preamble")
|
||||
}
|
||||
|
||||
// Prepend the preamble so wrapDollarVariables can parse the AST without
|
||||
// "undefined variable" errors for $-refs inside action blocks.
|
||||
wrapped, err := wrapDollarVariables(preamble+tmpl, structRootSet(data))
|
||||
if err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to rewrite template")
|
||||
}
|
||||
|
||||
result, err := buildDataMap(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &processingResult{Template: wrapped, Data: result, UnknownVars: unknownVars}, nil
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractFieldMappings(t *testing.T) {
|
||||
// Flat struct: mapstructure-tagged leaves only. Slices and interfaces are
|
||||
// dropped; maps (labels/annotations analogues) are kept as top-level leaves.
|
||||
type Flat struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Status string `mapstructure:"status"`
|
||||
UserCount int `mapstructure:"user_count"`
|
||||
IsActive bool `mapstructure:"is_active"`
|
||||
CreatedAt time.Time `mapstructure:"created_at"`
|
||||
Extra map[string]string `mapstructure:"extra"`
|
||||
Items []string `mapstructure:"items"` // slice skipped
|
||||
unexported string //nolint:unused // unexported skipped
|
||||
NoTag string // no mapstructure tag skipped
|
||||
SkippedTag string `mapstructure:"-"` // explicit skip
|
||||
}
|
||||
|
||||
// Nested struct: sub-struct paths are flattened into dotted mappings; the
|
||||
// parent path itself is also emitted so templates can bind `$alert := .alert`.
|
||||
type Inner struct {
|
||||
Value string `mapstructure:"value"`
|
||||
Op string `mapstructure:"op"`
|
||||
}
|
||||
type Outer struct {
|
||||
Alert struct {
|
||||
Status string `mapstructure:"status"`
|
||||
IsFiring bool `mapstructure:"is_firing"`
|
||||
} `mapstructure:"alert"`
|
||||
Rule struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Threshold Inner `mapstructure:"threshold"`
|
||||
} `mapstructure:"rule"`
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
data any
|
||||
expected []fieldPath
|
||||
}{
|
||||
{
|
||||
name: "flat struct surfaces only mapstructure-tagged scalars",
|
||||
data: Flat{},
|
||||
expected: []fieldPath{
|
||||
"name", "status", "user_count", "is_active", "created_at", "extra",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested struct emits parent and dotted leaf paths",
|
||||
data: Outer{},
|
||||
expected: []fieldPath{
|
||||
"alert",
|
||||
"alert.status",
|
||||
"alert.is_firing",
|
||||
"rule",
|
||||
"rule.name",
|
||||
"rule.threshold",
|
||||
"rule.threshold.value",
|
||||
"rule.threshold.op",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil data",
|
||||
data: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "non-struct type",
|
||||
data: "string",
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := extractFieldMappings(tc.data)
|
||||
require.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildVariableDefinitions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
tmpl string
|
||||
data any
|
||||
expectedVars []string // substrings that must appear in result
|
||||
forbiddenVars []string // substrings that must NOT appear (dotted identifiers)
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "empty template still emits struct bindings for title data",
|
||||
tmpl: "",
|
||||
data: &alertmanagertypes.NotificationTemplateData{Alert: alertmanagertypes.NotificationAlert{Receiver: "slack"}},
|
||||
expectedVars: []string{
|
||||
"{{ $alert := .alert }}",
|
||||
"{{ $rule := .rule }}",
|
||||
},
|
||||
// Dotted leaves are NOT emitted as preamble bindings — they're
|
||||
// reached via {{ index . "alert" "status" }} at expansion time.
|
||||
forbiddenVars: []string{
|
||||
"$alert.status",
|
||||
"$rule.threshold.value",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mix of known and unknown vars in alert body",
|
||||
tmpl: "$rule.name fired: $custom_label",
|
||||
data: &alertmanagertypes.AlertData{Rule: alertmanagertypes.RuleInfo{Name: "test"}, Alert: alertmanagertypes.AlertInfo{Status: "firing"}},
|
||||
expectedVars: []string{
|
||||
"{{ $alert := .alert }}",
|
||||
"{{ $rule := .rule }}",
|
||||
`{{ $custom_label := "<no value>" }}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "known dotted variables do not get flagged as unknown",
|
||||
tmpl: "$alert.is_firing and $rule.threshold.value",
|
||||
data: &alertmanagertypes.AlertData{},
|
||||
// $alert and $rule (first segments) are in mappings, so no unknown
|
||||
// stubs; the dotted leaves are resolved by WrapDollarVariables.
|
||||
expectedVars: []string{
|
||||
"{{ $alert := .alert }}",
|
||||
"{{ $rule := .rule }}",
|
||||
},
|
||||
forbiddenVars: []string{
|
||||
`"<no value>"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Label-derived $-refs aren't stubbed as unknown; their first
|
||||
// segment is marked known so {{ $severity := ... }} stubs don't
|
||||
// appear in the preamble. Resolution happens at expansion via the
|
||||
// root-level flattening performed in buildDataMap.
|
||||
name: "label first-segments suppress unknown-var stubs",
|
||||
tmpl: "$severity for $service $cloud.region.instance",
|
||||
data: &alertmanagertypes.NotificationTemplateData{Labels: template.KV{
|
||||
"severity": "critical",
|
||||
"service": "test",
|
||||
"cloud.region.instance": "ap-south-1",
|
||||
}},
|
||||
forbiddenVars: []string{
|
||||
`{{ $severity := "<no value>" }}`,
|
||||
`{{ $service := "<no value>" }}`,
|
||||
`{{ $cloud := "<no value>" }}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same rule holds for AlertData labels",
|
||||
tmpl: "$severity $service",
|
||||
data: &alertmanagertypes.AlertData{Labels: template.KV{
|
||||
"severity": "critical",
|
||||
"service": "test",
|
||||
}},
|
||||
forbiddenVars: []string{
|
||||
`{{ $severity := "<no value>" }}`,
|
||||
`{{ $service := "<no value>" }}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid template syntax returns error",
|
||||
tmpl: "{{invalid",
|
||||
data: &alertmanagertypes.NotificationTemplateData{},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, _, err := buildPreamble(tc.tmpl, tc.data)
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
for _, expected := range tc.expectedVars {
|
||||
require.Contains(t, result, expected, "expected preamble substring missing")
|
||||
}
|
||||
for _, forbidden := range tc.forbiddenVars {
|
||||
require.NotContains(t, result, forbidden, "unexpected preamble substring present")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreProcessTemplateAndData(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
tmpl string
|
||||
data any
|
||||
expectedTemplateContains []string
|
||||
expectedData map[string]any
|
||||
expectedUnknownVars map[string]bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "title template: struct-root walks and flat dotted label keys",
|
||||
tmpl: "[$alert.status] $rule.name (ID: $rule.id) firing=$alert.total_firing severity=$severity method=$http.request.method",
|
||||
data: &alertmanagertypes.NotificationTemplateData{
|
||||
Alert: alertmanagertypes.NotificationAlert{
|
||||
Receiver: "pagerduty",
|
||||
Status: "firing",
|
||||
TotalFiring: 3,
|
||||
},
|
||||
Rule: alertmanagertypes.RuleInfo{
|
||||
Name: "HighMemory",
|
||||
ID: "rule-123",
|
||||
},
|
||||
Labels: template.KV{
|
||||
"severity": "critical",
|
||||
"http.request.method": "GET",
|
||||
},
|
||||
},
|
||||
expectedTemplateContains: []string{
|
||||
"{{$alert := .alert}}",
|
||||
"{{$rule := .rule}}",
|
||||
`[{{ index . "alert" "status" }}] {{ index . "rule" "name" }} (ID: {{ index . "rule" "id" }})`,
|
||||
`firing={{ index . "alert" "total_firing" }} severity={{ .severity }}`,
|
||||
`method={{ index . "http.request.method" }}`,
|
||||
},
|
||||
expectedData: map[string]any{
|
||||
"alert": map[string]any{
|
||||
"receiver": "pagerduty",
|
||||
"status": "firing",
|
||||
"total_firing": 3,
|
||||
"total_resolved": 0,
|
||||
},
|
||||
"severity": "critical",
|
||||
"http.request.method": "GET",
|
||||
},
|
||||
expectedUnknownVars: map[string]bool{},
|
||||
},
|
||||
{
|
||||
name: "body template with nested threshold access and per-alert annotation",
|
||||
tmpl: "$rule.name: value $alert.value $rule.threshold.op $rule.threshold.value ($alert.status) desc=$description",
|
||||
data: &alertmanagertypes.AlertData{
|
||||
Alert: alertmanagertypes.AlertInfo{
|
||||
Status: "firing",
|
||||
Value: "85%",
|
||||
IsFiring: true,
|
||||
},
|
||||
Rule: alertmanagertypes.RuleInfo{
|
||||
Name: "DiskFull",
|
||||
ID: "disk-001",
|
||||
Severity: "warning",
|
||||
Threshold: alertmanagertypes.Threshold{Value: "80%", Op: ">"},
|
||||
},
|
||||
Annotations: template.KV{
|
||||
"description": "Disk full and cannot be written to",
|
||||
},
|
||||
},
|
||||
expectedTemplateContains: []string{
|
||||
"{{$alert := .alert}}",
|
||||
"{{$rule := .rule}}",
|
||||
// "description" is an annotation flattened to root; the preamble
|
||||
// now binds it off the root rather than via .annotations lookup.
|
||||
"{{$description := .description}}",
|
||||
`{{ index . "rule" "name" }}: value {{ index . "alert" "value" }} {{ index . "rule" "threshold" "op" }} {{ index . "rule" "threshold" "value" }}`,
|
||||
},
|
||||
expectedData: map[string]any{
|
||||
"description": "Disk full and cannot be written to",
|
||||
},
|
||||
expectedUnknownVars: map[string]bool{},
|
||||
},
|
||||
{
|
||||
// Struct roots reserve their first-segment namespace: a label
|
||||
// whose key starts with "alert." is shadowed by the Alert sub-map,
|
||||
// and must be accessed via the explicit $labels.* prefix.
|
||||
name: "label colliding with struct root is accessed via $labels.*",
|
||||
tmpl: "$alert.status via $labels.alert.custom",
|
||||
data: &alertmanagertypes.NotificationTemplateData{
|
||||
Alert: alertmanagertypes.NotificationAlert{Status: "firing"},
|
||||
Labels: template.KV{"alert.custom": "x"},
|
||||
},
|
||||
expectedTemplateContains: []string{
|
||||
`{{ index . "alert" "status" }}`,
|
||||
`{{ index .labels "alert.custom" }}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Same shadowing rule applies symmetrically to annotations.
|
||||
name: "annotation colliding with struct root is accessed via $annotations.*",
|
||||
tmpl: "$alert.status via $annotations.alert.meta",
|
||||
data: &alertmanagertypes.NotificationTemplateData{
|
||||
Alert: alertmanagertypes.NotificationAlert{Status: "firing"},
|
||||
Annotations: template.KV{"alert.meta": "x"},
|
||||
},
|
||||
expectedTemplateContains: []string{
|
||||
`{{ index . "alert" "status" }}`,
|
||||
`{{ index .annotations "alert.meta" }}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
// When a label and an annotation share a key, the label wins at the
|
||||
// root flattening layer. Users who want the annotation must address
|
||||
// it explicitly via $annotations.<key>.
|
||||
name: "label takes precedence over same-named annotation at root",
|
||||
tmpl: "flat=$env labels_only=$labels.env annotations_only=$annotations.env",
|
||||
data: &alertmanagertypes.NotificationTemplateData{
|
||||
Labels: template.KV{"env": "prod"},
|
||||
Annotations: template.KV{"env": "staging"},
|
||||
},
|
||||
expectedTemplateContains: []string{
|
||||
`flat={{ .env }}`,
|
||||
`labels_only={{ index .labels "env" }}`,
|
||||
`annotations_only={{ index .annotations "env" }}`,
|
||||
},
|
||||
expectedData: map[string]any{
|
||||
"env": "prod",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty template returns flattened data",
|
||||
tmpl: "",
|
||||
data: &alertmanagertypes.NotificationTemplateData{Alert: alertmanagertypes.NotificationAlert{Receiver: "slack"}},
|
||||
},
|
||||
{
|
||||
name: "invalid template syntax",
|
||||
tmpl: "{{invalid",
|
||||
data: &alertmanagertypes.NotificationTemplateData{},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "unknown dollar var in text renders empty",
|
||||
tmpl: "alert $custom_note fired",
|
||||
data: &alertmanagertypes.NotificationTemplateData{Rule: alertmanagertypes.RuleInfo{Name: "HighCPU"}},
|
||||
expectedTemplateContains: []string{
|
||||
`{{$custom_note := "<no value>"}}`,
|
||||
"alert {{ .custom_note }} fired",
|
||||
},
|
||||
expectedUnknownVars: map[string]bool{"custom_note": true},
|
||||
},
|
||||
{
|
||||
name: "unknown dollar var in action block renders empty",
|
||||
tmpl: "alert {{ $custom_note }} fired",
|
||||
data: &alertmanagertypes.NotificationTemplateData{Rule: alertmanagertypes.RuleInfo{Name: "HighCPU"}},
|
||||
expectedTemplateContains: []string{
|
||||
`{{$custom_note := "<no value>"}}`,
|
||||
`alert {{$custom_note}} fired`,
|
||||
},
|
||||
expectedUnknownVars: map[string]bool{"custom_note": true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := preProcessTemplateAndData(tc.tmpl, tc.data)
|
||||
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
if tc.tmpl == "" {
|
||||
require.Equal(t, "", result.Template)
|
||||
return
|
||||
}
|
||||
|
||||
for _, substr := range tc.expectedTemplateContains {
|
||||
require.Contains(t, result.Template, substr)
|
||||
}
|
||||
for k, v := range tc.expectedData {
|
||||
require.Equal(t, v, result.Data[k], "data[%q] mismatch", k)
|
||||
}
|
||||
if tc.expectedUnknownVars != nil {
|
||||
require.Equal(t, tc.expectedUnknownVars, result.UnknownVars)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template/parse"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
// bareVariableRegex matches $-references including dotted paths (e.g. $alert.is_firing).
|
||||
var bareVariableRegex = regexp.MustCompile(`\$(\w+(?:\.\w+)*)`)
|
||||
|
||||
// bareVariableFirstSegRegex captures only the first segment of a $-reference.
|
||||
// $labels.severity yields "$labels"; $alert.is_firing yields "$alert".
|
||||
var bareVariableFirstSegRegex = regexp.MustCompile(`\$\w+`)
|
||||
|
||||
// wrapDollarVariables rewrites bare $-references in a template's plain-text
|
||||
// regions into Go text/template syntax. References inside `{{ ... }}` action
|
||||
// blocks are left untouched — they're already template syntax. structRoots
|
||||
// is the set of top-level mapstructure names whose values are nested structs
|
||||
// (e.g. "alert", "rule"): $-refs whose first segment is in this set are
|
||||
// walked segment-by-segment; everything else dotted is treated as a flat key
|
||||
// on the root map so flattened OTel-style label keys resolve naturally.
|
||||
//
|
||||
// Examples (structRoots = {alert, rule}):
|
||||
//
|
||||
// "$name is $status" -> "{{ .name }} is {{ .status }}"
|
||||
// "$labels.severity" -> `{{ index .labels "severity" }}`
|
||||
// "$labels.http.method" -> `{{ index .labels "http.method" }}`
|
||||
// "$annotations.summary" -> `{{ index .annotations "summary" }}`
|
||||
// "$alert.is_firing" -> `{{ index . "alert" "is_firing" }}`
|
||||
// "$rule.threshold.value" -> `{{ index . "rule" "threshold" "value" }}`
|
||||
// "$service.name" -> `{{ index . "service.name" }}`
|
||||
// "$name is {{ .Status }}" -> "{{ .name }} is {{ .Status }}"
|
||||
func wrapDollarVariables(src string, structRoots map[string]bool) (string, error) {
|
||||
if src == "" {
|
||||
return src, nil
|
||||
}
|
||||
|
||||
tree := parse.New("template")
|
||||
tree.Mode = parse.SkipFuncCheck
|
||||
|
||||
if _, err := tree.Parse(src, "{{", "}}", make(map[string]*parse.Tree), nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
walkAndWrapTextNodes(tree.Root, structRoots)
|
||||
return tree.Root.String(), nil
|
||||
}
|
||||
|
||||
// walkAndWrapTextNodes descends the parse tree and rewrites $-references
|
||||
// found in TextNodes. If/Range bodies are recursed into. ActionNodes and
|
||||
// other `{{...}}` constructs are left alone because they're already template
|
||||
// syntax. WithNode is not yet supported — add when the editor grows a "with"
|
||||
// block.
|
||||
func walkAndWrapTextNodes(node parse.Node, structRoots map[string]bool) {
|
||||
if reflect.ValueOf(node).IsNil() {
|
||||
return
|
||||
}
|
||||
|
||||
switch n := node.(type) {
|
||||
case *parse.ListNode:
|
||||
if n.Nodes != nil {
|
||||
for _, child := range n.Nodes {
|
||||
walkAndWrapTextNodes(child, structRoots)
|
||||
}
|
||||
}
|
||||
|
||||
case *parse.TextNode:
|
||||
n.Text = bareVariableRegex.ReplaceAllFunc(n.Text, func(match []byte) []byte {
|
||||
return rewriteDollarRef(match, structRoots)
|
||||
})
|
||||
|
||||
case *parse.IfNode:
|
||||
walkAndWrapTextNodes(n.List, structRoots)
|
||||
walkAndWrapTextNodes(n.ElseList, structRoots)
|
||||
|
||||
case *parse.RangeNode:
|
||||
walkAndWrapTextNodes(n.List, structRoots)
|
||||
walkAndWrapTextNodes(n.ElseList, structRoots)
|
||||
}
|
||||
}
|
||||
|
||||
// rewriteDollarRef converts one $-reference (with the leading $) into the
|
||||
// corresponding Go template expression.
|
||||
//
|
||||
// - labels./annotations. prefixes: treat the remainder as a single map key
|
||||
// (OTel-style keys like "http.request.method" are flat keys, not paths).
|
||||
// - Dotted path with a struct-root first segment: walk via chained index
|
||||
// arguments. `index x a b c` is equivalent to x[a][b][c].
|
||||
// - Other dotted path: treat as a single flat key on the root map, so a
|
||||
// flattened OTel-style label key like "service.name" resolves.
|
||||
// - Simple names: plain dot access on the root map.
|
||||
func rewriteDollarRef(match []byte, structRoots map[string]bool) []byte {
|
||||
name := string(match[1:])
|
||||
|
||||
if !strings.Contains(name, ".") {
|
||||
return fmt.Appendf(nil, `{{ .%s }}`, name)
|
||||
}
|
||||
|
||||
if key, ok := strings.CutPrefix(name, "labels."); ok {
|
||||
return fmt.Appendf(nil, `{{ index .labels %q }}`, key)
|
||||
}
|
||||
if key, ok := strings.CutPrefix(name, "annotations."); ok {
|
||||
return fmt.Appendf(nil, `{{ index .annotations %q }}`, key)
|
||||
}
|
||||
|
||||
// If the first segment is a known struct root, walk segments.
|
||||
if dot := strings.IndexByte(name, '.'); dot >= 0 && structRoots[name[:dot]] {
|
||||
parts := strings.Split(name, ".")
|
||||
args := make([]string, len(parts))
|
||||
for i, p := range parts {
|
||||
args[i] = fmt.Sprintf("%q", p)
|
||||
}
|
||||
return fmt.Appendf(nil, `{{ index . %s }}`, strings.Join(args, " "))
|
||||
}
|
||||
|
||||
// Otherwise treat the full dotted path as a single flat root key.
|
||||
return fmt.Appendf(nil, `{{ index . %q }}`, name)
|
||||
}
|
||||
|
||||
// extractUsedVariables returns the set of every base $-name referenced in the
|
||||
// template — text nodes, action blocks, branch conditions, loop declarations.
|
||||
// Names are first-segment only: $labels.severity contributes "labels".
|
||||
//
|
||||
// The template is validated during extraction (a synthesised preamble
|
||||
// pre-declares each name so the parser doesn't trip on "undefined variable"
|
||||
// for names that genuinely exist in the data), so a parse error here
|
||||
// indicates a genuine syntax problem rather than a missing binding.
|
||||
func extractUsedVariables(src string) (map[string]bool, error) {
|
||||
if src == "" {
|
||||
return map[string]bool{}, nil
|
||||
}
|
||||
|
||||
used := make(map[string]bool)
|
||||
for _, m := range bareVariableFirstSegRegex.FindAll([]byte(src), -1) {
|
||||
used[string(m[1:])] = true
|
||||
}
|
||||
|
||||
var preamble strings.Builder
|
||||
for name := range used {
|
||||
fmt.Fprintf(&preamble, `{{$%s := ""}}`, name)
|
||||
}
|
||||
|
||||
tree := parse.New("template")
|
||||
tree.Mode = parse.SkipFuncCheck
|
||||
if _, err := tree.Parse(preamble.String()+src, "{{", "}}", make(map[string]*parse.Tree), nil); err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInternal, "failed to extract used variables")
|
||||
}
|
||||
|
||||
return used, nil
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
package alertmanagertemplate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWrapBareVars(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "mixed variables with actions",
|
||||
input: "$name is {{.Status}}",
|
||||
expected: "{{ .name }} is {{.Status}}",
|
||||
},
|
||||
{
|
||||
name: "nested variables in range",
|
||||
input: `{{range .items}}
|
||||
$title
|
||||
{{end}}`,
|
||||
expected: `{{range .items}}
|
||||
{{ .title }}
|
||||
{{end}}`,
|
||||
},
|
||||
{
|
||||
name: "nested variables in if else",
|
||||
input: "{{if .ok}}$a{{else}}$b{{end}}",
|
||||
expected: "{{if .ok}}{{ .a }}{{else}}{{ .b }}{{end}}",
|
||||
},
|
||||
// Labels prefix: index into .labels map
|
||||
{
|
||||
name: "labels variables prefix simple",
|
||||
input: "$labels.service",
|
||||
expected: `{{ index .labels "service" }}`,
|
||||
},
|
||||
{
|
||||
name: "labels variables prefix nested with multiple dots",
|
||||
input: "$labels.http.status",
|
||||
expected: `{{ index .labels "http.status" }}`,
|
||||
},
|
||||
{
|
||||
name: "multiple labels variables simple and nested",
|
||||
input: "$labels.service and $labels.instance.id",
|
||||
expected: `{{ index .labels "service" }} and {{ index .labels "instance.id" }}`,
|
||||
},
|
||||
// Annotations prefix: index into .annotations map
|
||||
{
|
||||
name: "annotations variables prefix simple",
|
||||
input: "$annotations.summary",
|
||||
expected: `{{ index .annotations "summary" }}`,
|
||||
},
|
||||
{
|
||||
name: "annotations variables prefix nested with multiple dots",
|
||||
input: "$annotations.alert.url",
|
||||
expected: `{{ index .annotations "alert.url" }}`,
|
||||
},
|
||||
// Struct-root paths: walk segment-by-segment via chained index.
|
||||
{
|
||||
name: "struct-root dotted path walks via chained index",
|
||||
input: "$alert.is_firing",
|
||||
expected: `{{ index . "alert" "is_firing" }}`,
|
||||
},
|
||||
{
|
||||
name: "deeply nested struct-root path",
|
||||
input: "$rule.threshold.value",
|
||||
expected: `{{ index . "rule" "threshold" "value" }}`,
|
||||
},
|
||||
// Non-struct-root dotted paths: treated as a single flat key on the
|
||||
// root map, so flattened OTel-style label keys resolve naturally.
|
||||
{
|
||||
name: "non-struct-root dotted path hits flat root key",
|
||||
input: "$service.name",
|
||||
expected: `{{ index . "service.name" }}`,
|
||||
},
|
||||
// Hybrid: all types combined
|
||||
{
|
||||
name: "hybrid - all variables types",
|
||||
input: "Alert: $alert_name Labels: $labels.severity Annotations: $annotations.desc Value: $alert.value Count: $error_count",
|
||||
expected: `Alert: {{ .alert_name }} Labels: {{ index .labels "severity" }} Annotations: {{ index .annotations "desc" }} Value: {{ index . "alert" "value" }} Count: {{ .error_count }}`,
|
||||
},
|
||||
{
|
||||
name: "already wrapped should not be changed",
|
||||
input: "{{$status := .status}}{{.name}} is {{$status | toUpper}}",
|
||||
expected: "{{$status := .status}}{{.name}} is {{$status | toUpper}}",
|
||||
},
|
||||
{
|
||||
name: "no variables should not be changed",
|
||||
input: "Hello world",
|
||||
expected: "Hello world",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "deeply nested",
|
||||
input: "{{range .items}}{{if .ok}}$deep{{end}}{{end}}",
|
||||
expected: "{{range .items}}{{if .ok}}{{ .deep }}{{end}}{{end}}",
|
||||
},
|
||||
{
|
||||
name: "complex example",
|
||||
input: `Hello $name, your score is $score.
|
||||
{{if .isAdmin}}
|
||||
Welcome back $name, you have {{.unreadCount}} messages.
|
||||
{{end}}`,
|
||||
expected: `Hello {{ .name }}, your score is {{ .score }}.
|
||||
{{if .isAdmin}}
|
||||
Welcome back {{ .name }}, you have {{.unreadCount}} messages.
|
||||
{{end}}`,
|
||||
},
|
||||
{
|
||||
name: "with custom function",
|
||||
input: "$name triggered at {{urlescape .url}}",
|
||||
expected: "{{ .name }} triggered at {{urlescape .url}}",
|
||||
},
|
||||
{
|
||||
name: "invalid template",
|
||||
input: "{{invalid",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
// structRoots used across the test cases: "alert" and "rule" are walked,
|
||||
// anything else dotted is treated as a flat root-map key.
|
||||
structRoots := map[string]bool{"alert": true, "rule": true}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := wrapDollarVariables(tc.input, structRoots)
|
||||
|
||||
if tc.expectError {
|
||||
require.Error(t, err, "should error on invalid template syntax")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractUsedVariables(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected map[string]bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "simple usage in text",
|
||||
input: "$name is $status",
|
||||
expected: map[string]bool{"name": true, "status": true},
|
||||
},
|
||||
{
|
||||
name: "declared in action block",
|
||||
input: "{{ $name := .name }}",
|
||||
expected: map[string]bool{"name": true},
|
||||
},
|
||||
{
|
||||
name: "range loop vars",
|
||||
input: "{{ range $i, $v := .items }}{{ end }}",
|
||||
expected: map[string]bool{"i": true, "v": true},
|
||||
},
|
||||
{
|
||||
name: "mixed text and action",
|
||||
input: "$x and {{ $y }}",
|
||||
expected: map[string]bool{"x": true, "y": true},
|
||||
},
|
||||
{
|
||||
name: "dotted path in text extracts base only",
|
||||
input: "$labels.severity",
|
||||
expected: map[string]bool{"labels": true},
|
||||
},
|
||||
{
|
||||
name: "nested if else",
|
||||
input: "{{ if .ok }}{{ $a }}{{ else }}{{ $b }}{{ end }}",
|
||||
expected: map[string]bool{"a": true, "b": true},
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: map[string]bool{},
|
||||
},
|
||||
{
|
||||
name: "no variables",
|
||||
input: "Hello world",
|
||||
expected: map[string]bool{},
|
||||
},
|
||||
{
|
||||
name: "invalid template returns error",
|
||||
input: "{{invalid",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := extractUsedVariables(tc.input)
|
||||
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ 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"
|
||||
@@ -60,7 +59,6 @@ type provider struct {
|
||||
cloudIntegrationHandler cloudintegration.Handler
|
||||
ruleStateHistoryHandler rulestatehistory.Handler
|
||||
alertmanagerHandler alertmanager.Handler
|
||||
rulerHandler ruler.Handler
|
||||
}
|
||||
|
||||
func NewFactory(
|
||||
@@ -88,7 +86,6 @@ 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(
|
||||
@@ -119,7 +116,6 @@ func NewFactory(
|
||||
cloudIntegrationHandler,
|
||||
ruleStateHistoryHandler,
|
||||
alertmanagerHandler,
|
||||
rulerHandler,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -152,7 +148,6 @@ 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()
|
||||
@@ -183,7 +178,6 @@ func newProvider(
|
||||
cloudIntegrationHandler: cloudIntegrationHandler,
|
||||
ruleStateHistoryHandler: ruleStateHistoryHandler,
|
||||
alertmanagerHandler: alertmanagerHandler,
|
||||
rulerHandler: rulerHandler,
|
||||
}
|
||||
|
||||
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
|
||||
@@ -288,10 +282,6 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addRulerRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
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,8 +2,6 @@ package global
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
|
||||
@@ -39,34 +37,5 @@ 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 "/"
|
||||
}
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1070,23 +1070,11 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
|
||||
|
||||
serviceNameToTotalDurationMap = tracedetail.CalculateServiceTime(serviceNameIntervalMap)
|
||||
|
||||
traceCache := model.GetWaterfallSpansForTraceWithMetadataCache{
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
DurationNano: durationNano,
|
||||
TotalSpans: totalSpans,
|
||||
TotalErrorSpans: totalErrorSpans,
|
||||
SpanIdToSpanNodeMap: spanIdToSpanNodeMap,
|
||||
ServiceNameToTotalDurationMap: serviceNameToTotalDurationMap,
|
||||
TraceRoots: traceRoots,
|
||||
HasMissingSpans: hasMissingSpans,
|
||||
}
|
||||
// TODO: set the span data (model.GetWaterfallSpansForTraceWithMetadataCache) in cache here
|
||||
// removed existing cache usage since it was not getting used due to this bug https://github.com/SigNoz/engineering-pod/issues/4648
|
||||
// and was causing out of memory issues https://github.com/SigNoz/engineering-pod/issues/4638
|
||||
|
||||
r.logger.Info("getWaterfallSpansForTraceWithMetadata: processing pre cache", "duration", time.Since(processingBeforeCache), "traceID", traceID)
|
||||
cacheErr := r.cacheForTraceDetail.Set(ctx, orgID, strings.Join([]string{"getWaterfallSpansForTraceWithMetadata", traceID}, "-"), &traceCache, time.Minute*5)
|
||||
if cacheErr != nil {
|
||||
r.logger.Debug("failed to store cache for getWaterfallSpansForTraceWithMetadata", "traceID", traceID, errorsV2.Attr(err))
|
||||
}
|
||||
}
|
||||
|
||||
processingPostCache := time.Now()
|
||||
@@ -1259,19 +1247,13 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, orgID
|
||||
}
|
||||
|
||||
selectedSpans = tracedetail.GetAllSpansForFlamegraph(traceRoots, spanIdToSpanNodeMap)
|
||||
traceCache := model.GetFlamegraphSpansForTraceCache{
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
DurationNano: durationNano,
|
||||
SelectedSpans: selectedSpans,
|
||||
TraceRoots: traceRoots,
|
||||
}
|
||||
|
||||
// TODO: set the trace data (model.GetFlamegraphSpansForTraceCache) in cache here
|
||||
// removed existing cache usage since it was not getting used due to this bug https://github.com/SigNoz/engineering-pod/issues/4648
|
||||
// and was causing out of memory issues https://github.com/SigNoz/engineering-pod/issues/4638
|
||||
|
||||
r.logger.Info("getFlamegraphSpansForTrace: processing pre cache", "duration", time.Since(processingBeforeCache), "traceID", traceID)
|
||||
cacheErr := r.cacheForTraceDetail.Set(ctx, orgID, strings.Join([]string{"getFlamegraphSpansForTrace", traceID}, "-"), &traceCache, time.Minute*5)
|
||||
if cacheErr != nil {
|
||||
r.logger.Debug("failed to store cache for getFlamegraphSpansForTrace", "traceID", traceID, errorsV2.Attr(err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
processingPostCache := time.Now()
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/modules/thirdpartyapi"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
|
||||
"io"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net/http"
|
||||
@@ -78,7 +78,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
)
|
||||
|
||||
@@ -98,7 +98,7 @@ func NewRouter() *mux.Router {
|
||||
type APIHandler struct {
|
||||
logger *slog.Logger
|
||||
reader interfaces.Reader
|
||||
ruleManager ruler.Ruler
|
||||
ruleManager *rules.Manager
|
||||
querier interfaces.Querier
|
||||
querierV2 interfaces.Querier
|
||||
queryBuilder *queryBuilder.QueryBuilder
|
||||
@@ -150,6 +150,9 @@ 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
|
||||
|
||||
@@ -205,7 +208,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.Signoz.Ruler,
|
||||
ruleManager: opts.RuleManager,
|
||||
IntegrationsController: opts.IntegrationsController,
|
||||
CloudIntegrationsController: opts.CloudIntegrationsController,
|
||||
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
||||
@@ -505,6 +508,12 @@ 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)
|
||||
@@ -578,6 +587,7 @@ 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)
|
||||
|
||||
@@ -593,6 +603,26 @@ 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 {
|
||||
|
||||
@@ -648,164 +678,127 @@ func (aH *APIHandler) PopulateTemporality(ctx context.Context, orgID valuer.UUID
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
func (aH *APIHandler) listDowntimeSchedules(w http.ResponseWriter, r *http.Request) {
|
||||
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
|
||||
if errv2 != nil {
|
||||
render.Error(w, errv2)
|
||||
return
|
||||
}
|
||||
|
||||
// todo(amol): need to add sorter
|
||||
|
||||
aH.Respond(w, rules)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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) 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 {
|
||||
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) testRule(w http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
schedules, err := aH.ruleManager.MaintenanceStore().GetAllPlannedMaintenance(r.Context(), claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
|
||||
// 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
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getDowntimeSchedule(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
|
||||
}
|
||||
defer r.Body.Close()
|
||||
body, err := io.ReadAll(r.Body)
|
||||
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 {
|
||||
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)
|
||||
if err := schedule.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"alertCount": alertCount,
|
||||
"message": "notification sent",
|
||||
_, err = aH.ruleManager.MaintenanceStore().CreatePlannedMaintenance(r.Context(), schedule)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
aH.Respond(w, response)
|
||||
aH.Respond(w, nil)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) editDowntimeSchedule(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
|
||||
}
|
||||
|
||||
var schedule ruletypes.GettablePlannedMaintenance
|
||||
err = json.NewDecoder(r.Body).Decode(&schedule)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
if err := schedule.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = aH.ruleManager.MaintenanceStore().EditPlannedMaintenance(r.Context(), schedule, id)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, nil)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) deleteDowntimeSchedule(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
|
||||
}
|
||||
|
||||
err = aH.ruleManager.MaintenanceStore().DeletePlannedMaintenance(r.Context(), id)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, nil)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getRuleStats(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1019,6 +1012,19 @@ 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
|
||||
|
||||
@@ -1220,6 +1226,142 @@ 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,16 +9,24 @@ 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"
|
||||
@@ -26,7 +34,10 @@ 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"
|
||||
@@ -36,13 +47,15 @@ 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
|
||||
config signoz.Config
|
||||
signoz *signoz.SigNoz
|
||||
ruleManager *rules.Manager
|
||||
|
||||
// public http router
|
||||
httpConn net.Listener
|
||||
@@ -89,6 +102,24 @@ 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,
|
||||
@@ -100,6 +131,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
|
||||
apiHandler, err := NewAPIHandler(APIHandlerOpts{
|
||||
Reader: reader,
|
||||
RuleManager: rm,
|
||||
IntegrationsController: integrationsController,
|
||||
CloudIntegrationsController: cloudIntegrationsController,
|
||||
LogsParsingPipelineController: logParsingPipelineController,
|
||||
@@ -114,7 +146,8 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
|
||||
s := &Server{
|
||||
config: config,
|
||||
signoz: signoz,
|
||||
signoz: signoz,
|
||||
ruleManager: rm,
|
||||
httpHostPort: constants.HTTPHostPort,
|
||||
unavailableChannel: make(chan healthcheck.Status),
|
||||
}
|
||||
@@ -211,20 +244,6 @@ 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
|
||||
@@ -251,6 +270,8 @@ 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
|
||||
@@ -294,6 +315,55 @@ 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.StorableRule{
|
||||
storedRule := &ruletypes.Rule{
|
||||
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.ListPlannedMaintenance(ctx, g.orgID.StringValue())
|
||||
maintenance, err := g.maintenanceStore.GetAllPlannedMaintenance(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.ListPlannedMaintenance(ctx, g.orgID.StringValue())
|
||||
maintenance, err := g.maintenanceStore.GetAllPlannedMaintenance(ctx, g.orgID.StringValue())
|
||||
|
||||
if err != nil {
|
||||
g.logger.ErrorContext(ctx, "error in processing sql query", errors.Attr(err))
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package ruler
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
EvalDelay time.Duration `mapstructure:"eval_delay"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
@@ -15,9 +12,7 @@ func NewConfigFactory() factory.ConfigFactory {
|
||||
}
|
||||
|
||||
func newConfig() factory.Config {
|
||||
return Config{
|
||||
EvalDelay: 2 * time.Minute,
|
||||
}
|
||||
return Config{}
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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,48 +1,7 @@
|
||||
package ruler
|
||||
|
||||
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"
|
||||
)
|
||||
import "github.com/SigNoz/signoz/pkg/statsreporter"
|
||||
|
||||
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.StorableRule, fn func(context.Context, valuer.UUID) error) (valuer.UUID, error) {
|
||||
func (m *MockSQLRuleStore) CreateRule(ctx context.Context, rule *ruletypes.Rule, 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.StorableRule, fn func(context.Context) error) error {
|
||||
func (m *MockSQLRuleStore) EditRule(ctx context.Context, rule *ruletypes.Rule, 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.StorableRule, error) {
|
||||
func (m *MockSQLRuleStore) GetStoredRule(ctx context.Context, id valuer.UUID) (*ruletypes.Rule, 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.StorableRule, error) {
|
||||
func (m *MockSQLRuleStore) GetStoredRules(ctx context.Context, orgID string) ([]*ruletypes.Rule, 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.StorableRule) {
|
||||
func (m *MockSQLRuleStore) ExpectCreateRule(rule *ruletypes.Rule) {
|
||||
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.StorableRule) {
|
||||
}
|
||||
|
||||
// ExpectEditRule sets up SQL expectations for EditRule operation.
|
||||
func (m *MockSQLRuleStore) ExpectEditRule(rule *ruletypes.StorableRule) {
|
||||
func (m *MockSQLRuleStore) ExpectEditRule(rule *ruletypes.Rule) {
|
||||
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.StorableRule) {
|
||||
func (m *MockSQLRuleStore) ExpectGetStoredRule(ruleID valuer.UUID, rule *ruletypes.Rule) {
|
||||
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.StorableRule) {
|
||||
func (m *MockSQLRuleStore) ExpectGetStoredRules(orgID string, rules []*ruletypes.Rule) {
|
||||
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.StorableRule) {
|
||||
func (m *MockSQLRuleStore) ExpectGetStoredRulesByMetricName(orgID string, metricName string, rules []*ruletypes.Rule) {
|
||||
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) ListPlannedMaintenance(ctx context.Context, orgID string) ([]*ruletypes.PlannedMaintenance, error) {
|
||||
gettableMaintenancesRules := make([]*ruletypes.PlannedMaintenanceWithRules, 0)
|
||||
func (r *maintenance) GetAllPlannedMaintenance(ctx context.Context, orgID string) ([]*ruletypes.GettablePlannedMaintenance, error) {
|
||||
gettableMaintenancesRules := make([]*ruletypes.GettablePlannedMaintenanceRule, 0)
|
||||
err := r.sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
@@ -33,16 +33,16 @@ func (r *maintenance) ListPlannedMaintenance(ctx context.Context, orgID string)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gettablePlannedMaintenance := make([]*ruletypes.PlannedMaintenance, 0)
|
||||
gettablePlannedMaintenance := make([]*ruletypes.GettablePlannedMaintenance, 0)
|
||||
for _, gettableMaintenancesRule := range gettableMaintenancesRules {
|
||||
gettablePlannedMaintenance = append(gettablePlannedMaintenance, gettableMaintenancesRule.ToPlannedMaintenance())
|
||||
gettablePlannedMaintenance = append(gettablePlannedMaintenance, gettableMaintenancesRule.ConvertGettableMaintenanceRuleToGettableMaintenance())
|
||||
}
|
||||
|
||||
return gettablePlannedMaintenance, nil
|
||||
}
|
||||
|
||||
func (r *maintenance) GetPlannedMaintenanceByID(ctx context.Context, id valuer.UUID) (*ruletypes.PlannedMaintenance, error) {
|
||||
storableMaintenanceRule := new(ruletypes.PlannedMaintenanceWithRules)
|
||||
func (r *maintenance) GetPlannedMaintenanceByID(ctx context.Context, id valuer.UUID) (*ruletypes.GettablePlannedMaintenance, error) {
|
||||
storableMaintenanceRule := new(ruletypes.GettablePlannedMaintenanceRule)
|
||||
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, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "planned maintenance with ID: %s does not exist", id.StringValue())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return storableMaintenanceRule.ToPlannedMaintenance(), nil
|
||||
return storableMaintenanceRule.ConvertGettableMaintenanceRuleToGettableMaintenance(), nil
|
||||
}
|
||||
|
||||
func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance *ruletypes.PostablePlannedMaintenance) (*ruletypes.PlannedMaintenance, error) {
|
||||
func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance ruletypes.GettablePlannedMaintenance) (valuer.UUID, error) {
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return valuer.UUID{}, 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.AlertIds {
|
||||
for _, ruleIDStr := range maintenance.RuleIDs {
|
||||
ruleID, err := valuer.NewUUID(ruleIDStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return valuer.UUID{}, err
|
||||
}
|
||||
|
||||
maintenanceRules = append(maintenanceRules, &ruletypes.StorablePlannedMaintenanceRule{
|
||||
@@ -122,20 +122,10 @@ func (r *maintenance) CreatePlannedMaintenance(ctx context.Context, maintenance
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return valuer.UUID{}, err
|
||||
}
|
||||
|
||||
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
|
||||
return storablePlannedMaintenance.ID, nil
|
||||
}
|
||||
|
||||
func (r *maintenance) DeletePlannedMaintenance(ctx context.Context, id valuer.UUID) error {
|
||||
@@ -152,27 +142,22 @@ func (r *maintenance) DeletePlannedMaintenance(ctx context.Context, id valuer.UU
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance *ruletypes.PostablePlannedMaintenance, id valuer.UUID) error {
|
||||
func (r *maintenance) EditPlannedMaintenance(ctx context.Context, maintenance ruletypes.GettablePlannedMaintenance, 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: existing.CreatedAt,
|
||||
CreatedAt: maintenance.CreatedAt,
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: existing.CreatedBy,
|
||||
CreatedBy: maintenance.CreatedBy,
|
||||
UpdatedBy: claims.Email,
|
||||
},
|
||||
Name: maintenance.Name,
|
||||
@@ -182,7 +167,7 @@ func (r *maintenance) UpdatePlannedMaintenance(ctx context.Context, maintenance
|
||||
}
|
||||
|
||||
storablePlannedMaintenanceRules := make([]*ruletypes.StorablePlannedMaintenanceRule, 0)
|
||||
for _, ruleIDStr := range maintenance.AlertIds {
|
||||
for _, ruleIDStr := range maintenance.RuleIDs {
|
||||
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.StorableRule, cb func(context.Context, valuer.UUID) error) (valuer.UUID, error) {
|
||||
func (r *rule) CreateRule(ctx context.Context, storedRule *ruletypes.Rule, 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.StorableRul
|
||||
return storedRule.ID, nil
|
||||
}
|
||||
|
||||
func (r *rule) EditRule(ctx context.Context, storedRule *ruletypes.StorableRule, cb func(context.Context) error) error {
|
||||
func (r *rule) EditRule(ctx context.Context, storedRule *ruletypes.Rule, 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.StorableRule)).
|
||||
Model(new(ruletypes.Rule)).
|
||||
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.StorableRule, error) {
|
||||
rules := make([]*ruletypes.StorableRule, 0)
|
||||
func (r *rule) GetStoredRules(ctx context.Context, orgID string) ([]*ruletypes.Rule, error) {
|
||||
rules := make([]*ruletypes.Rule, 0)
|
||||
err := r.sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
@@ -102,8 +102,8 @@ func (r *rule) GetStoredRules(ctx context.Context, orgID string) ([]*ruletypes.S
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (r *rule) GetStoredRule(ctx context.Context, id valuer.UUID) (*ruletypes.StorableRule, error) {
|
||||
rule := new(ruletypes.StorableRule)
|
||||
func (r *rule) GetStoredRule(ctx context.Context, id valuer.UUID) (*ruletypes.Rule, error) {
|
||||
rule := new(ruletypes.Rule)
|
||||
err := r.sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
@@ -111,7 +111,7 @@ func (r *rule) GetStoredRule(ctx context.Context, id valuer.UUID) (*ruletypes.St
|
||||
Where("id = ?", id.StringValue()).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "rule with ID: %s does not exist", id.StringValue())
|
||||
return nil, err
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
@@ -1,323 +0,0 @@
|
||||
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,93 +3,27 @@ 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(
|
||||
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 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 (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 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) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
|
||||
@@ -100,35 +34,3 @@ 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,15 +305,6 @@ 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,8 +3,6 @@ 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"
|
||||
@@ -67,7 +65,6 @@ type Handlers struct {
|
||||
CloudIntegrationHandler cloudintegration.Handler
|
||||
RuleStateHistory rulestatehistory.Handler
|
||||
AlertmanagerHandler alertmanager.Handler
|
||||
RulerHandler ruler.Handler
|
||||
}
|
||||
|
||||
func NewHandlers(
|
||||
@@ -84,7 +81,6 @@ func NewHandlers(
|
||||
zeusService zeus.Zeus,
|
||||
registryHandler factory.Handler,
|
||||
alertmanagerService alertmanager.Alertmanager,
|
||||
rulerService ruler.Ruler,
|
||||
) Handlers {
|
||||
return Handlers{
|
||||
SavedView: implsavedview.NewHandler(modules.SavedView),
|
||||
@@ -108,6 +104,5 @@ 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, nil)
|
||||
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler, alertmanager)
|
||||
reflectVal := reflect.ValueOf(handlers)
|
||||
for i := 0; i < reflectVal.NumField(); i++ {
|
||||
f := reflectVal.Field(i)
|
||||
|
||||
@@ -31,7 +31,6 @@ 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"
|
||||
@@ -72,7 +71,6 @@ 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,6 +44,9 @@ 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"
|
||||
@@ -85,9 +88,9 @@ func NewCacheProviderFactories() factory.NamedMap[factory.ProviderFactory[cache.
|
||||
)
|
||||
}
|
||||
|
||||
func NewWebProviderFactories(globalConfig global.Config) factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]] {
|
||||
func NewWebProviderFactories() factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
routerweb.NewFactory(globalConfig),
|
||||
routerweb.NewFactory(),
|
||||
noopweb.NewFactory(),
|
||||
)
|
||||
}
|
||||
@@ -226,7 +229,11 @@ 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(
|
||||
@@ -282,7 +289,6 @@ 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(global.Config{})
|
||||
NewWebProviderFactories()
|
||||
})
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
@@ -64,6 +64,11 @@ 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,14 +26,12 @@ 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"
|
||||
@@ -77,7 +75,6 @@ type SigNoz struct {
|
||||
Tokenizer pkgtokenizer.Tokenizer
|
||||
IdentNResolver identn.IdentNResolver
|
||||
Authz authz.AuthZ
|
||||
Ruler ruler.Ruler
|
||||
Modules Modules
|
||||
Handlers Handlers
|
||||
QueryParser queryparser.QueryParser
|
||||
@@ -106,7 +103,6 @@ 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")
|
||||
@@ -365,6 +361,18 @@ 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 {
|
||||
@@ -433,12 +441,6 @@ 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)
|
||||
@@ -454,7 +456,7 @@ func New(
|
||||
// Create a list of all stats collectors
|
||||
statsCollectors := []statsreporter.StatsCollector{
|
||||
alertmanager,
|
||||
rulerInstance,
|
||||
ruler,
|
||||
modules.Dashboard,
|
||||
modules.SavedView,
|
||||
modules.UserSetter,
|
||||
@@ -491,7 +493,6 @@ 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
|
||||
@@ -499,7 +500,7 @@ func New(
|
||||
|
||||
// Initialize all handlers for the modules
|
||||
registryHandler := factory.NewHandler(registry)
|
||||
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager, rulerInstance)
|
||||
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager)
|
||||
|
||||
// Initialize the API server (after registry so it can access service health)
|
||||
apiserverInstance, err := factory.NewProviderFromNamedMap(
|
||||
@@ -533,7 +534,6 @@ func New(
|
||||
Tokenizer: tokenizer,
|
||||
IdentNResolver: identNResolver,
|
||||
Authz: authz,
|
||||
Ruler: rulerInstance,
|
||||
Modules: modules,
|
||||
Handlers: handlers,
|
||||
QueryParser: queryParser,
|
||||
|
||||
@@ -11,22 +11,19 @@ import (
|
||||
alertmanagertemplate "github.com/prometheus/alertmanager/template"
|
||||
)
|
||||
|
||||
func AdditionalFuncMap() tmpltext.FuncMap {
|
||||
return tmpltext.FuncMap{
|
||||
// urlescape escapes the string for use in a URL query parameter.
|
||||
// It returns tmplhtml.HTML to prevent the template engine from escaping the already escaped string.
|
||||
// url.QueryEscape escapes spaces as "+", and html/template escapes "+" as "+" if tmplhtml.HTML is not used.
|
||||
"urlescape": func(value string) tmplhtml.HTML {
|
||||
return tmplhtml.HTML(url.QueryEscape(value))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// customTemplateOption returns an Option that adds custom functions to the template.
|
||||
func customTemplateOption() alertmanagertemplate.Option {
|
||||
return func(text *tmpltext.Template, html *tmplhtml.Template) {
|
||||
text.Funcs(AdditionalFuncMap())
|
||||
html.Funcs(AdditionalFuncMap())
|
||||
funcs := tmpltext.FuncMap{
|
||||
// urlescape escapes the string for use in a URL query parameter.
|
||||
// It returns tmplhtml.HTML to prevent the template engine from escaping the already escaped string.
|
||||
// url.QueryEscape escapes spaces as "+", and html/template escapes "+" as "+" if tmplhtml.HTML is not used.
|
||||
"urlescape": func(value string) tmplhtml.HTML {
|
||||
return tmplhtml.HTML(url.QueryEscape(value))
|
||||
},
|
||||
}
|
||||
text.Funcs(funcs)
|
||||
html.Funcs(funcs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
package alertmanagertypes
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
)
|
||||
|
||||
// privateAnnotationPrefix marks annotations the rule manager attaches for
|
||||
// alertmanager-internal use (raw template strings, threshold metadata, link
|
||||
// targets). Annotations whose key starts with this prefix are stripped from
|
||||
// any surface that ends up visible to a template author or a notification
|
||||
// recipient; the alertmanager reads them off the raw alert before stripping.
|
||||
const privateAnnotationPrefix = "_"
|
||||
|
||||
// IsPrivateAnnotation reports whether an annotation key is considered
|
||||
// private — i.e. internal to alertmanager and should not be rendered in
|
||||
// notifications.
|
||||
func IsPrivateAnnotation(key string) bool {
|
||||
return strings.HasPrefix(key, privateAnnotationPrefix)
|
||||
}
|
||||
|
||||
// FilterPublicAnnotations returns a copy of kv with all private-prefixed
|
||||
// keys removed. Callers that expose annotations to templates or notification
|
||||
// payloads should pass them through this first.
|
||||
func FilterPublicAnnotations(kv template.KV) template.KV {
|
||||
out := make(template.KV, len(kv))
|
||||
for k, v := range kv {
|
||||
if IsPrivateAnnotation(k) {
|
||||
continue
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ExpandRequest carries the title/body templates and their defaults handed to
|
||||
// Templater.Expand. Default templates are used when the custom templates
|
||||
// expand to empty strings.
|
||||
type ExpandRequest struct {
|
||||
TitleTemplate string
|
||||
BodyTemplate string
|
||||
DefaultTitleTemplate string
|
||||
DefaultBodyTemplate string
|
||||
}
|
||||
|
||||
// ExpandResult is the rendered output of Templater.Expand.
|
||||
type ExpandResult struct {
|
||||
// Title is the expanded notification title (plain text).
|
||||
Title string
|
||||
// Body is the expanded notification body, one entry per input alert. The
|
||||
// body template is applied per-alert and concatenated by the caller.
|
||||
Body []string
|
||||
// IsDefaultBody is true when Body came from the default template (no
|
||||
// user-authored body was supplied), false when a custom template was used.
|
||||
IsDefaultBody bool
|
||||
// MissingVars is the union of $-references in the title and body templates
|
||||
// that did not resolve to any known field. Surfaced for preview warnings;
|
||||
// at runtime these render as "<no value>".
|
||||
MissingVars []string
|
||||
// NotificationData is the aggregate data that fed the title template,
|
||||
// exposed so callers can reuse it when rendering a channel-specific layout
|
||||
// (e.g. the email HTML shell) without rebuilding it from the alerts.
|
||||
NotificationData *NotificationTemplateData
|
||||
}
|
||||
|
||||
// AlertData holds per-alert data used when expanding body templates.
|
||||
//
|
||||
// Field paths follow OpenTelemetry-style dotted namespaces (via mapstructure
|
||||
// tags) so user templates can reference paths like $alert.is_firing,
|
||||
// $rule.threshold.value, or $log.url. JSON tags use camelCase for the wire
|
||||
// format.
|
||||
type AlertData struct {
|
||||
Alert AlertInfo `json:"alert" mapstructure:"alert"`
|
||||
Rule RuleInfo `json:"rule" mapstructure:"rule"`
|
||||
Log LinkInfo `json:"log" mapstructure:"log"`
|
||||
Trace LinkInfo `json:"trace" mapstructure:"trace"`
|
||||
Labels template.KV `json:"labels" mapstructure:"labels"`
|
||||
Annotations template.KV `json:"annotations" mapstructure:"annotations"`
|
||||
}
|
||||
|
||||
// AlertInfo holds the per-alert state and timing data.
|
||||
type AlertInfo struct {
|
||||
Status string `json:"status" mapstructure:"status"`
|
||||
Receiver string `json:"receiver" mapstructure:"receiver"`
|
||||
Value string `json:"value" mapstructure:"value"`
|
||||
StartsAt time.Time `json:"startsAt" mapstructure:"starts_at"`
|
||||
EndsAt time.Time `json:"endsAt" mapstructure:"ends_at"`
|
||||
GeneratorURL string `json:"generatorURL" mapstructure:"generator_url"`
|
||||
Fingerprint string `json:"fingerprint" mapstructure:"fingerprint"`
|
||||
IsFiring bool `json:"isFiring" mapstructure:"is_firing"`
|
||||
IsResolved bool `json:"isResolved" mapstructure:"is_resolved"`
|
||||
IsMissingData bool `json:"isMissingData" mapstructure:"is_missing_data"`
|
||||
IsRecovering bool `json:"isRecovering" mapstructure:"is_recovering"`
|
||||
}
|
||||
|
||||
// RuleInfo holds the rule metadata extracted from well-known labels and
|
||||
// annotations.
|
||||
type RuleInfo struct {
|
||||
Name string `json:"name" mapstructure:"name"`
|
||||
ID string `json:"id" mapstructure:"id"`
|
||||
URL string `json:"url" mapstructure:"url"`
|
||||
Severity string `json:"severity" mapstructure:"severity"`
|
||||
MatchType string `json:"matchType" mapstructure:"match_type"`
|
||||
Threshold Threshold `json:"threshold" mapstructure:"threshold"`
|
||||
}
|
||||
|
||||
// Threshold describes the breach condition.
|
||||
type Threshold struct {
|
||||
Value string `json:"value" mapstructure:"value"`
|
||||
Op string `json:"op" mapstructure:"op"`
|
||||
}
|
||||
|
||||
// LinkInfo groups a single URL so templates can reference $log.url and
|
||||
// $trace.url uniformly.
|
||||
type LinkInfo struct {
|
||||
URL string `json:"url" mapstructure:"url"`
|
||||
}
|
||||
|
||||
// NotificationTemplateData is the top-level data struct provided to title
|
||||
// templates, representing the aggregate of a grouped notification.
|
||||
type NotificationTemplateData struct {
|
||||
Alert NotificationAlert `json:"alert" mapstructure:"alert"`
|
||||
Rule RuleInfo `json:"rule" mapstructure:"rule"`
|
||||
Labels template.KV `json:"labels" mapstructure:"labels"`
|
||||
Annotations template.KV `json:"annotations" mapstructure:"annotations"`
|
||||
CommonLabels template.KV `json:"commonLabels" mapstructure:"common_labels"`
|
||||
CommonAnnotations template.KV `json:"commonAnnotations" mapstructure:"common_annotations"`
|
||||
GroupLabels template.KV `json:"groupLabels" mapstructure:"group_labels"`
|
||||
ExternalURL string `json:"externalURL" mapstructure:"external_url"`
|
||||
|
||||
// Per-alert data kept for body expansion; not exposed to the title template.
|
||||
Alerts []AlertData `json:"-" mapstructure:"-"`
|
||||
}
|
||||
|
||||
// NotificationAlert holds the aggregate alert fields available to title
|
||||
// templates (counts, overall status, receiver).
|
||||
type NotificationAlert struct {
|
||||
Receiver string `json:"receiver" mapstructure:"receiver"`
|
||||
Status string `json:"status" mapstructure:"status"`
|
||||
TotalFiring int `json:"totalFiring" mapstructure:"total_firing"`
|
||||
TotalResolved int `json:"totalResolved" mapstructure:"total_resolved"`
|
||||
}
|
||||
@@ -71,15 +71,6 @@ 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
|
||||
|
||||
@@ -93,32 +84,23 @@ var (
|
||||
QueryTypePromQL = QueryType{valuer.NewString("promql")}
|
||||
)
|
||||
|
||||
// Enum implements jsonschema.Enum; returns the acceptable values for QueryType.
|
||||
func (QueryType) Enum() []any {
|
||||
return []any{
|
||||
QueryTypeBuilder,
|
||||
QueryTypeClickHouseSQL,
|
||||
QueryTypePromQL,
|
||||
}
|
||||
}
|
||||
|
||||
type AlertCompositeQuery struct {
|
||||
Queries []qbtypes.QueryEnvelope `json:"queries" required:"true"`
|
||||
Queries []qbtypes.QueryEnvelope `json:"queries"`
|
||||
|
||||
PanelType PanelType `json:"panelType" required:"true"`
|
||||
QueryType QueryType `json:"queryType" required:"true"`
|
||||
PanelType PanelType `json:"panelType"`
|
||||
QueryType QueryType `json:"queryType"`
|
||||
// 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" required:"true"`
|
||||
CompareOperator CompareOperator `json:"op" required:"true"`
|
||||
CompositeQuery *AlertCompositeQuery `json:"compositeQuery"`
|
||||
CompareOperator CompareOperator `json:"op"`
|
||||
Target *float64 `json:"target,omitempty"`
|
||||
AlertOnAbsent bool `json:"alertOnAbsent,omitempty"`
|
||||
AbsentFor uint64 `json:"absentFor,omitempty"`
|
||||
MatchType MatchType `json:"matchType" required:"true"`
|
||||
MatchType MatchType `json:"matchType"`
|
||||
TargetUnit string `json:"targetUnit,omitempty"`
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
Seasonality Seasonality `json:"seasonality,omitzero"`
|
||||
|
||||
@@ -11,7 +11,6 @@ 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"
|
||||
@@ -26,16 +25,6 @@ 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"
|
||||
@@ -49,14 +38,14 @@ const (
|
||||
|
||||
// PostableRule is used to create alerting rule from HTTP api.
|
||||
type PostableRule struct {
|
||||
AlertName string `json:"alert" required:"true"`
|
||||
AlertName string `json:"alert"`
|
||||
AlertType AlertType `json:"alertType,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
RuleType RuleType `json:"ruleType,omitzero" required:"true"`
|
||||
RuleType RuleType `json:"ruleType,omitzero"`
|
||||
EvalWindow valuer.TextDuration `json:"evalWindow,omitzero"`
|
||||
Frequency valuer.TextDuration `json:"frequency,omitzero"`
|
||||
|
||||
RuleCondition *RuleCondition `json:"condition,omitempty" required:"true"`
|
||||
RuleCondition *RuleCondition `json:"condition,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
|
||||
@@ -597,24 +586,19 @@ 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" required:"true"`
|
||||
State AlertState `json:"state" required:"true"`
|
||||
Id string `json:"id"`
|
||||
State AlertState `json:"state"`
|
||||
PostableRule
|
||||
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"`
|
||||
CreatedAt *time.Time `json:"createAt"`
|
||||
CreatedBy *string `json:"createBy"`
|
||||
UpdatedAt *time.Time `json:"updateAt"`
|
||||
UpdatedBy *string `json:"updateBy"`
|
||||
}
|
||||
|
||||
func (g *GettableRule) MarshalJSON() ([]byte, error) {
|
||||
@@ -641,57 +625,3 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,33 +9,4 @@ const (
|
||||
LabelSeverityName = "severity"
|
||||
LabelLastSeen = "lastSeen"
|
||||
LabelRuleID = "ruleId"
|
||||
LabelRuleSource = "ruleSource"
|
||||
LabelNoData = "nodata"
|
||||
LabelTestAlert = "testalert"
|
||||
LabelAlertName = "alertname"
|
||||
LabelIsRecovering = "is_recovering"
|
||||
)
|
||||
|
||||
// Annotations set by the rule manager and consumed by the alertmanager
|
||||
// templating layer.
|
||||
//
|
||||
// Those prefixed with "_" are private: they're stripped from
|
||||
// notification-visible surfaces by alertmanagertypes.FilterPublicAnnotations
|
||||
// before rendering. Only the raw template strings are private — echoing
|
||||
// them into a notification is circular and never useful.
|
||||
//
|
||||
// The rest are public: they describe the firing alert (the breached value,
|
||||
// the configured threshold, the comparator, the match type, and deep links
|
||||
// to relevant logs/traces) and users may reference them directly as
|
||||
// {{ .Annotations.value }}, {{ .Annotations.threshold.value }}, etc. in
|
||||
// their channel templates.
|
||||
const (
|
||||
AnnotationTitleTemplate = "_title_template"
|
||||
AnnotationBodyTemplate = "_body_template"
|
||||
AnnotationRelatedLogs = "related_logs"
|
||||
AnnotationRelatedTraces = "related_traces"
|
||||
AnnotationValue = "value"
|
||||
AnnotationThresholdValue = "threshold.value"
|
||||
AnnotationCompareOp = "compare_op"
|
||||
AnnotationMatchType = "match_type"
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
type EvaluationKind struct {
|
||||
@@ -18,22 +17,14 @@ 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" required:"true"`
|
||||
Frequency valuer.TextDuration `json:"frequency" required:"true"`
|
||||
EvalWindow valuer.TextDuration `json:"evalWindow"`
|
||||
Frequency valuer.TextDuration `json:"frequency"`
|
||||
}
|
||||
|
||||
func (rollingWindow RollingWindow) Validate() error {
|
||||
@@ -55,13 +46,13 @@ func (rollingWindow RollingWindow) GetFrequency() valuer.TextDuration {
|
||||
}
|
||||
|
||||
type CumulativeWindow struct {
|
||||
Schedule CumulativeSchedule `json:"schedule" required:"true"`
|
||||
Frequency valuer.TextDuration `json:"frequency" required:"true"`
|
||||
Timezone string `json:"timezone" required:"true"`
|
||||
Schedule CumulativeSchedule `json:"schedule"`
|
||||
Frequency valuer.TextDuration `json:"frequency"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
|
||||
type CumulativeSchedule struct {
|
||||
Type ScheduleType `json:"type" required:"true"`
|
||||
Type ScheduleType `json:"type"`
|
||||
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
|
||||
@@ -79,16 +70,6 @@ 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 {
|
||||
@@ -244,31 +225,8 @@ func (cumulativeWindow CumulativeWindow) GetFrequency() valuer.TextDuration {
|
||||
}
|
||||
|
||||
type EvaluationEnvelope struct {
|
||||
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{},
|
||||
}
|
||||
Kind EvaluationKind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
|
||||
func (e *EvaluationEnvelope) UnmarshalJSON(data []byte) error {
|
||||
|
||||
@@ -15,42 +15,6 @@ 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
|
||||
@@ -62,63 +26,18 @@ type StorablePlannedMaintenance struct {
|
||||
OrgID string `bun:"org_id,type:text"`
|
||||
}
|
||||
|
||||
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"`
|
||||
type GettablePlannedMaintenance struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
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
|
||||
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"`
|
||||
}
|
||||
|
||||
type StorablePlannedMaintenanceRule struct {
|
||||
@@ -128,12 +47,12 @@ type StorablePlannedMaintenanceRule struct {
|
||||
RuleID valuer.UUID `bun:"rule_id,type:text"`
|
||||
}
|
||||
|
||||
type PlannedMaintenanceWithRules struct {
|
||||
type GettablePlannedMaintenanceRule struct {
|
||||
*StorablePlannedMaintenance `bun:",extend"`
|
||||
Rules []*StorablePlannedMaintenanceRule `bun:"rel:has-many,join:id=planned_maintenance_id"`
|
||||
}
|
||||
|
||||
func (m *PlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bool {
|
||||
func (m *GettablePlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bool {
|
||||
// Check if the alert ID is in the maintenance window
|
||||
found := false
|
||||
if len(m.RuleIDs) > 0 {
|
||||
@@ -203,7 +122,7 @@ func (m *PlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bool {
|
||||
|
||||
// checkDaily rebases the recurrence start to today (or yesterday if needed)
|
||||
// and returns true if currentTime is within [candidate, candidate+Duration].
|
||||
func (m *PlannedMaintenance) checkDaily(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
func (m *GettablePlannedMaintenance) 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,
|
||||
@@ -218,7 +137,7 @@ func (m *PlannedMaintenance) checkDaily(currentTime time.Time, rec *Recurrence,
|
||||
// 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 *PlannedMaintenance) checkWeekly(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
func (m *GettablePlannedMaintenance) 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)
|
||||
@@ -250,7 +169,7 @@ func (m *PlannedMaintenance) checkWeekly(currentTime time.Time, rec *Recurrence,
|
||||
|
||||
// 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 *PlannedMaintenance) checkMonthly(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
|
||||
func (m *GettablePlannedMaintenance) 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()
|
||||
@@ -282,7 +201,7 @@ func (m *PlannedMaintenance) checkMonthly(currentTime time.Time, rec *Recurrence
|
||||
return currentTime.Sub(candidate) <= rec.Duration.Duration()
|
||||
}
|
||||
|
||||
func (m *PlannedMaintenance) IsActive(now time.Time) bool {
|
||||
func (m *GettablePlannedMaintenance) IsActive(now time.Time) bool {
|
||||
ruleID := "maintenance"
|
||||
if len(m.RuleIDs) > 0 {
|
||||
ruleID = (m.RuleIDs)[0]
|
||||
@@ -290,7 +209,7 @@ func (m *PlannedMaintenance) IsActive(now time.Time) bool {
|
||||
return m.ShouldSkip(ruleID, now)
|
||||
}
|
||||
|
||||
func (m *PlannedMaintenance) IsUpcoming() bool {
|
||||
func (m *GettablePlannedMaintenance) IsUpcoming() bool {
|
||||
loc, err := time.LoadLocation(m.Schedule.Timezone)
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -306,11 +225,11 @@ func (m *PlannedMaintenance) IsUpcoming() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *PlannedMaintenance) IsRecurring() bool {
|
||||
func (m *GettablePlannedMaintenance) IsRecurring() bool {
|
||||
return m.Schedule.Recurrence != nil
|
||||
}
|
||||
|
||||
func (m *PlannedMaintenance) Validate() error {
|
||||
func (m *GettablePlannedMaintenance) Validate() error {
|
||||
if m.Name == "" {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing name in the payload")
|
||||
}
|
||||
@@ -333,7 +252,7 @@ func (m *PlannedMaintenance) Validate() error {
|
||||
}
|
||||
|
||||
if m.Schedule.Recurrence != nil {
|
||||
if m.Schedule.Recurrence.RepeatType.IsZero() {
|
||||
if m.Schedule.Recurrence.RepeatType == "" {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing repeat type in the payload")
|
||||
}
|
||||
if m.Schedule.Recurrence.Duration.IsZero() {
|
||||
@@ -346,38 +265,38 @@ func (m *PlannedMaintenance) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m PlannedMaintenance) MarshalJSON() ([]byte, error) {
|
||||
func (m GettablePlannedMaintenance) MarshalJSON() ([]byte, error) {
|
||||
now := time.Now().In(time.FixedZone(m.Schedule.Timezone, 0))
|
||||
var status MaintenanceStatus
|
||||
var status string
|
||||
if m.IsActive(now) {
|
||||
status = MaintenanceStatusActive
|
||||
status = "active"
|
||||
} else if m.IsUpcoming() {
|
||||
status = MaintenanceStatusUpcoming
|
||||
status = "upcoming"
|
||||
} else {
|
||||
status = MaintenanceStatusExpired
|
||||
status = "expired"
|
||||
}
|
||||
var kind MaintenanceKind
|
||||
var kind string
|
||||
|
||||
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() && m.Schedule.EndTime.After(m.Schedule.StartTime) {
|
||||
kind = MaintenanceKindFixed
|
||||
kind = "fixed"
|
||||
} else {
|
||||
kind = MaintenanceKindRecurring
|
||||
kind = "recurring"
|
||||
}
|
||||
|
||||
return json.Marshal(struct {
|
||||
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 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: m.ID,
|
||||
Id: m.Id,
|
||||
Name: m.Name,
|
||||
Description: m.Description,
|
||||
Schedule: m.Schedule,
|
||||
@@ -391,7 +310,7 @@ func (m PlannedMaintenance) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
}
|
||||
|
||||
func (m *PlannedMaintenanceWithRules) ToPlannedMaintenance() *PlannedMaintenance {
|
||||
func (m *GettablePlannedMaintenanceRule) ConvertGettableMaintenanceRuleToGettableMaintenance() *GettablePlannedMaintenance {
|
||||
ruleIDs := []string{}
|
||||
if m.Rules != nil {
|
||||
for _, storableMaintenanceRule := range m.Rules {
|
||||
@@ -399,8 +318,8 @@ func (m *PlannedMaintenanceWithRules) ToPlannedMaintenance() *PlannedMaintenance
|
||||
}
|
||||
}
|
||||
|
||||
return &PlannedMaintenance{
|
||||
ID: m.ID,
|
||||
return &GettablePlannedMaintenance{
|
||||
Id: m.ID.StringValue(),
|
||||
Name: m.Name,
|
||||
Description: m.Description,
|
||||
Schedule: m.Schedule,
|
||||
@@ -412,15 +331,10 @@ func (m *PlannedMaintenanceWithRules) ToPlannedMaintenance() *PlannedMaintenance
|
||||
}
|
||||
}
|
||||
|
||||
type ListPlannedMaintenanceParams struct {
|
||||
Active *bool `query:"active"`
|
||||
Recurring *bool `query:"recurring"`
|
||||
}
|
||||
|
||||
type MaintenanceStore interface {
|
||||
CreatePlannedMaintenance(context.Context, *PostablePlannedMaintenance) (*PlannedMaintenance, error)
|
||||
CreatePlannedMaintenance(context.Context, GettablePlannedMaintenance) (valuer.UUID, 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)
|
||||
GetPlannedMaintenanceByID(context.Context, valuer.UUID) (*GettablePlannedMaintenance, error)
|
||||
EditPlannedMaintenance(context.Context, GettablePlannedMaintenance, valuer.UUID) error
|
||||
GetAllPlannedMaintenance(context.Context, string) ([]*GettablePlannedMaintenance, error)
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
maintenance *PlannedMaintenance
|
||||
maintenance *GettablePlannedMaintenance
|
||||
ts time.Time
|
||||
skip bool
|
||||
}{
|
||||
{
|
||||
name: "only-on-saturday",
|
||||
maintenance: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -204,7 +204,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
// Different timezone tests
|
||||
{
|
||||
name: "timezone-offset-test",
|
||||
maintenance: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -301,7 +301,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "daily-maintenance-crosses-midnight",
|
||||
maintenance: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -316,7 +316,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "monthly-maintenance-crosses-month-end",
|
||||
maintenance: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -346,7 +346,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "weekly-maintenance-crosses-midnight",
|
||||
maintenance: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -377,7 +377,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "daily-maintenance-crosses-midnight",
|
||||
maintenance: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
@@ -407,7 +407,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "fixed planned maintenance start <= ts <= end",
|
||||
maintenance: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
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: &PlannedMaintenance{
|
||||
maintenance: &GettablePlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
|
||||
@@ -8,52 +8,26 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type RepeatType struct {
|
||||
valuer.String
|
||||
}
|
||||
type RepeatType string
|
||||
|
||||
var (
|
||||
RepeatTypeDaily = RepeatType{valuer.NewString("daily")}
|
||||
RepeatTypeWeekly = RepeatType{valuer.NewString("weekly")}
|
||||
RepeatTypeMonthly = RepeatType{valuer.NewString("monthly")}
|
||||
const (
|
||||
RepeatTypeDaily RepeatType = "daily"
|
||||
RepeatTypeWeekly RepeatType = "weekly"
|
||||
RepeatTypeMonthly RepeatType = "monthly"
|
||||
)
|
||||
|
||||
// Enum implements jsonschema.Enum; returns the acceptable values for RepeatType.
|
||||
func (RepeatType) Enum() []any {
|
||||
return []any{
|
||||
RepeatTypeDaily,
|
||||
RepeatTypeWeekly,
|
||||
RepeatTypeMonthly,
|
||||
}
|
||||
}
|
||||
type RepeatOn string
|
||||
|
||||
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")}
|
||||
const (
|
||||
RepeatOnSunday RepeatOn = "sunday"
|
||||
RepeatOnMonday RepeatOn = "monday"
|
||||
RepeatOnTuesday RepeatOn = "tuesday"
|
||||
RepeatOnWednesday RepeatOn = "wednesday"
|
||||
RepeatOnThursday RepeatOn = "thursday"
|
||||
RepeatOnFriday RepeatOn = "friday"
|
||||
RepeatOnSaturday RepeatOn = "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,
|
||||
@@ -65,10 +39,10 @@ var RepeatOnAllMap = map[RepeatOn]time.Weekday{
|
||||
}
|
||||
|
||||
type Recurrence struct {
|
||||
StartTime time.Time `json:"startTime" required:"true"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime *time.Time `json:"endTime,omitempty"`
|
||||
Duration valuer.TextDuration `json:"duration" required:"true"`
|
||||
RepeatType RepeatType `json:"repeatType" required:"true"`
|
||||
Duration valuer.TextDuration `json:"duration"`
|
||||
RepeatType RepeatType `json:"repeatType"`
|
||||
RepeatOn []RepeatOn `json:"repeatOn"`
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type StorableRule struct {
|
||||
type Rule struct {
|
||||
bun.BaseModel `bun:"table:rule"`
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
@@ -20,7 +20,7 @@ type StorableRule struct {
|
||||
OrgID string `bun:"org_id,type:text"`
|
||||
}
|
||||
|
||||
func NewStatsFromRules(rules []*StorableRule) map[string]any {
|
||||
func NewStatsFromRules(rules []*Rule) 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, *StorableRule, func(context.Context, valuer.UUID) error) (valuer.UUID, error)
|
||||
EditRule(context.Context, *StorableRule, func(context.Context) error) error
|
||||
CreateRule(context.Context, *Rule, func(context.Context, valuer.UUID) error) (valuer.UUID, error)
|
||||
EditRule(context.Context, *Rule, func(context.Context) error) error
|
||||
DeleteRule(context.Context, valuer.UUID, func(context.Context) error) error
|
||||
GetStoredRules(context.Context, string) ([]*StorableRule, error)
|
||||
GetStoredRule(context.Context, valuer.UUID) (*StorableRule, error)
|
||||
GetStoredRules(context.Context, string) ([]*Rule, error)
|
||||
GetStoredRule(context.Context, valuer.UUID) (*Rule, error)
|
||||
GetStoredRulesByMetricName(context.Context, string, string) ([]RuleAlert, error)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
type Schedule struct {
|
||||
Timezone string `json:"timezone" required:"true"`
|
||||
Timezone string `json:"timezone"`
|
||||
StartTime time.Time `json:"startTime,omitempty"`
|
||||
EndTime time.Time `json:"endTime,omitempty"`
|
||||
Recurrence *Recurrence `json:"recurrence"`
|
||||
|
||||
@@ -11,7 +11,6 @@ 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 {
|
||||
@@ -22,32 +21,9 @@ 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" 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{},
|
||||
}
|
||||
Kind ThresholdKind `json:"kind"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
|
||||
func (r *RuleThresholdData) UnmarshalJSON(data []byte) error {
|
||||
@@ -112,12 +88,12 @@ type RuleThreshold interface {
|
||||
}
|
||||
|
||||
type BasicRuleThreshold struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
TargetValue *float64 `json:"target" required:"true"`
|
||||
Name string `json:"name"`
|
||||
TargetValue *float64 `json:"target"`
|
||||
TargetUnit string `json:"targetUnit"`
|
||||
RecoveryTarget *float64 `json:"recoveryTarget"`
|
||||
MatchType MatchType `json:"matchType" required:"true"`
|
||||
CompareOperator CompareOperator `json:"op" required:"true"`
|
||||
MatchType MatchType `json:"matchType"`
|
||||
CompareOperator CompareOperator `json:"op"`
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,10 @@ import (
|
||||
type Config struct {
|
||||
// Whether the web package is enabled.
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
|
||||
// The name of the index file to serve.
|
||||
Index string `mapstructure:"index"`
|
||||
|
||||
// The directory from which to serve the web files.
|
||||
// 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.
|
||||
Directory string `mapstructure:"directory"`
|
||||
}
|
||||
|
||||
@@ -23,7 +22,7 @@ func NewConfigFactory() factory.ConfigFactory {
|
||||
func newConfig() factory.Config {
|
||||
return &Config{
|
||||
Enabled: true,
|
||||
Index: "index.html",
|
||||
Prefix: "/",
|
||||
Directory: "/etc/signoz/web",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
)
|
||||
|
||||
func TestNewWithEnvProvider(t *testing.T) {
|
||||
t.Setenv("SIGNOZ_WEB_PREFIX", "/web")
|
||||
t.Setenv("SIGNOZ_WEB_ENABLED", "false")
|
||||
|
||||
conf, err := config.New(
|
||||
@@ -36,7 +37,7 @@ func TestNewWithEnvProvider(t *testing.T) {
|
||||
|
||||
expected := &Config{
|
||||
Enabled: false,
|
||||
Index: def.Index,
|
||||
Prefix: "/web",
|
||||
Directory: def.Directory,
|
||||
}
|
||||
|
||||
|
||||
@@ -8,55 +8,56 @@ 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
|
||||
indexContents []byte
|
||||
fileHandler http.Handler
|
||||
config web.Config
|
||||
}
|
||||
|
||||
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 NewFactory() factory.ProviderFactory[web.Web, web.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("router"), New)
|
||||
}
|
||||
|
||||
func New(ctx context.Context, settings factory.ProviderSettings, config web.Config, globalConfig global.Config) (web.Web, error) {
|
||||
func New(ctx context.Context, settings factory.ProviderSettings, config web.Config) (web.Web, error) {
|
||||
fi, err := os.Stat(config.Directory)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot access web directory")
|
||||
}
|
||||
|
||||
if !fi.IsDir() {
|
||||
ok := fi.IsDir()
|
||||
if !ok {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "web directory is not a directory")
|
||||
}
|
||||
|
||||
indexPath := filepath.Join(config.Directory, config.Index)
|
||||
raw, err := os.ReadFile(indexPath)
|
||||
fi, err = os.Stat(filepath.Join(config.Directory, indexFileName))
|
||||
if err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot read %q in web directory", config.Index)
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot access %q in web directory", 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()})
|
||||
if os.IsNotExist(err) || fi.IsDir() {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "%q does not exist", indexFileName)
|
||||
}
|
||||
|
||||
return &provider{
|
||||
config: config,
|
||||
indexContents: indexContents,
|
||||
fileHandler: http.FileServer(http.Dir(config.Directory)),
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
cache := middleware.NewCache(0)
|
||||
err := router.PathPrefix("/").
|
||||
err := router.PathPrefix(provider.config.Prefix).
|
||||
Handler(
|
||||
cache.Wrap(http.HandlerFunc(provider.ServeHTTP)),
|
||||
http.StripPrefix(
|
||||
provider.config.Prefix,
|
||||
cache.Wrap(http.HandlerFunc(provider.ServeHTTP)),
|
||||
),
|
||||
).GetError()
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "unable to add web to router")
|
||||
@@ -74,7 +75,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) {
|
||||
provider.serveIndex(rw)
|
||||
http.ServeFile(rw, req, filepath.Join(provider.config.Directory, indexFileName))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -86,15 +87,10 @@ func (provider *provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
if fi.IsDir() {
|
||||
// path is a directory, serve index.html
|
||||
provider.serveIndex(rw)
|
||||
http.ServeFile(rw, req, filepath.Join(provider.config.Directory, indexFileName))
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, use http.FileServer to serve the static file
|
||||
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)
|
||||
http.FileServer(http.Dir(provider.config.Directory)).ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
@@ -5,113 +5,45 @@ 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 startServer(t *testing.T, config web.Config, globalConfig global.Config) string {
|
||||
t.Helper()
|
||||
func TestServeHttpWithoutPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
fi, err := os.Open(filepath.Join("testdata", indexFileName))
|
||||
require.NoError(t, err)
|
||||
|
||||
web, err := New(context.Background(), factorytest.NewSettings(), config, globalConfig)
|
||||
expected, err := io.ReadAll(fi)
|
||||
require.NoError(t, err)
|
||||
|
||||
web, err := New(context.Background(), factorytest.NewSettings(), web.Config{Prefix: "/", Directory: filepath.Join("testdata")})
|
||||
require.NoError(t, err)
|
||||
|
||||
router := mux.NewRouter()
|
||||
require.NoError(t, web.AddToRouter(router))
|
||||
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) }()
|
||||
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>`,
|
||||
},
|
||||
server := &http.Server{
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
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)
|
||||
go func() {
|
||||
_ = server.Serve(listener)
|
||||
}()
|
||||
defer func() {
|
||||
_ = server.Close()
|
||||
}()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -122,7 +54,11 @@ func TestServeNoTemplateIndex(t *testing.T) {
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
name: "NonExistentPath",
|
||||
name: "Index",
|
||||
path: "/" + indexFileName,
|
||||
},
|
||||
{
|
||||
name: "DoesNotExist",
|
||||
path: "/does-not-exist",
|
||||
},
|
||||
{
|
||||
@@ -131,55 +67,104 @@ func TestServeNoTemplateIndex(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
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{})
|
||||
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)
|
||||
|
||||
assert.Equal(t, string(expected), httpGet(t, base+testCase.path))
|
||||
defer func() {
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
|
||||
actual, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestServeInvalidTemplateIndex(t *testing.T) {
|
||||
func TestServeHttpWithPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expected, err := os.ReadFile(filepath.Join("testdata", "invalid_template.html"))
|
||||
fi, err := os.Open(filepath.Join("testdata", indexFileName))
|
||||
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
|
||||
name string
|
||||
path string
|
||||
found bool
|
||||
}{
|
||||
{
|
||||
name: "Root",
|
||||
path: "/",
|
||||
name: "Root",
|
||||
path: "/web",
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "NonExistentPath",
|
||||
path: "/does-not-exist",
|
||||
name: "Index",
|
||||
path: "/web/" + indexFileName,
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "Directory",
|
||||
path: "/assets",
|
||||
name: "FileDoesNotExist",
|
||||
path: "/web/does-not-exist",
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "Directory",
|
||||
path: "/web/assets",
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "DoesNotExist",
|
||||
path: "/does-not-exist",
|
||||
found: false,
|
||||
},
|
||||
}
|
||||
|
||||
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"}})
|
||||
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)
|
||||
}
|
||||
|
||||
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
Normal file
3
pkg/web/routerweb/testdata/assets/index.css
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
#root {
|
||||
background-color: red;
|
||||
}
|
||||
1
pkg/web/routerweb/testdata/assets/style.css
vendored
1
pkg/web/routerweb/testdata/assets/style.css
vendored
@@ -1 +0,0 @@
|
||||
body { color: red; }
|
||||
1
pkg/web/routerweb/testdata/index.html
vendored
Normal file
1
pkg/web/routerweb/testdata/index.html
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<h1>Welcome to test data!!!</h1>
|
||||
@@ -1 +0,0 @@
|
||||
<html><head><base href="[[." /></head><body>Bad template</body></html>
|
||||
1
pkg/web/routerweb/testdata/no_template.html
vendored
1
pkg/web/routerweb/testdata/no_template.html
vendored
@@ -1 +0,0 @@
|
||||
<html><head></head><body>No template here</body></html>
|
||||
@@ -1 +0,0 @@
|
||||
<html><head><base href="[[.BaseHref]]" /></head><body>Welcome to test data!!!</body></html>
|
||||
@@ -1,42 +0,0 @@
|
||||
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