Initial commit
This commit is contained in:
commit
8160adcdca
8 changed files with 990 additions and 0 deletions
91
checker/options.go
Normal file
91
checker/options.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright 2020-2026 The happyDomain Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package checker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// GetOption extracts a typed value from checker options, handling both
|
||||
// native Go types (in-process providers) and map[string]any values
|
||||
// (from JSON round-tripping through HTTP providers). Returns the zero
|
||||
// value and false if the key is missing or the value cannot be converted.
|
||||
func GetOption[T any](options CheckerOptions, key string) (T, bool) {
|
||||
v, ok := options[key]
|
||||
if !ok {
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// Direct type assertion (in-process path).
|
||||
if t, ok := v.(T); ok {
|
||||
return t, true
|
||||
}
|
||||
|
||||
// JSON round-trip for values deserialized as map[string]any over HTTP.
|
||||
raw, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
var t T
|
||||
if err := json.Unmarshal(raw, &t); err != nil {
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
return t, true
|
||||
}
|
||||
|
||||
// GetFloatOption extracts a float64 from checker options, handling both
|
||||
// native float64 values and json.Number. Returns defaultVal if the key
|
||||
// is missing or the value cannot be converted.
|
||||
func GetFloatOption(options CheckerOptions, key string, defaultVal float64) float64 {
|
||||
v, ok := options[key]
|
||||
if !ok {
|
||||
return defaultVal
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return val
|
||||
case json.Number:
|
||||
f, err := val.Float64()
|
||||
if err != nil {
|
||||
return defaultVal
|
||||
}
|
||||
return f
|
||||
default:
|
||||
return defaultVal
|
||||
}
|
||||
}
|
||||
|
||||
// GetIntOption extracts an int from checker options, using GetFloatOption
|
||||
// internally. Returns defaultVal if the key is missing or invalid.
|
||||
func GetIntOption(options CheckerOptions, key string, defaultVal int) int {
|
||||
return int(GetFloatOption(options, key, float64(defaultVal)))
|
||||
}
|
||||
|
||||
// GetBoolOption extracts a bool from checker options.
|
||||
// Returns defaultVal if the key is missing or the value is not a bool.
|
||||
func GetBoolOption(options CheckerOptions, key string, defaultVal bool) bool {
|
||||
v, ok := options[key]
|
||||
if !ok {
|
||||
return defaultVal
|
||||
}
|
||||
b, ok := v.(bool)
|
||||
if !ok {
|
||||
return defaultVal
|
||||
}
|
||||
return b
|
||||
}
|
||||
78
checker/registry.go
Normal file
78
checker/registry.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright 2020-2026 The happyDomain Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package checker
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
// checkerRegistry is the global registry for checker definitions.
|
||||
// Thread-safety: all writes happen during init() before any goroutines start.
|
||||
// After initialization, the map is read-only and safe for concurrent access.
|
||||
var checkerRegistry = map[string]*CheckerDefinition{}
|
||||
|
||||
// observationProviderRegistry is the global registry for observation providers,
|
||||
// keyed by ObservationKey.
|
||||
var observationProviderRegistry = map[ObservationKey]ObservationProvider{}
|
||||
|
||||
// RegisterChecker registers a checker definition globally.
|
||||
func RegisterChecker(c *CheckerDefinition) {
|
||||
log.Println("Registering new checker:", c.ID)
|
||||
c.BuildRulesInfo()
|
||||
checkerRegistry[c.ID] = c
|
||||
}
|
||||
|
||||
// RegisterExternalizableChecker registers a checker that supports being
|
||||
// delegated to a remote HTTP endpoint. It appends an "endpoint" AdminOpt
|
||||
// so the administrator can optionally configure a remote URL.
|
||||
// When the endpoint is left empty, the checker runs locally as usual.
|
||||
func RegisterExternalizableChecker(c *CheckerDefinition) {
|
||||
c.Options.AdminOpts = append(c.Options.AdminOpts,
|
||||
CheckerOptionDocumentation{
|
||||
Id: "endpoint",
|
||||
Type: "string",
|
||||
Label: "Remote checker endpoint URL",
|
||||
Description: "If set, delegate observation collection to this HTTP endpoint instead of running locally.",
|
||||
Placeholder: "http://checker-" + c.ID + ":8080",
|
||||
NoOverride: true,
|
||||
},
|
||||
)
|
||||
RegisterChecker(c)
|
||||
}
|
||||
|
||||
// RegisterObservationProvider registers an observation provider globally.
|
||||
func RegisterObservationProvider(p ObservationProvider) {
|
||||
observationProviderRegistry[p.Key()] = p
|
||||
}
|
||||
|
||||
// GetCheckers returns all registered checker definitions.
|
||||
func GetCheckers() map[string]*CheckerDefinition {
|
||||
return checkerRegistry
|
||||
}
|
||||
|
||||
// FindChecker returns the checker definition with the given ID, or nil.
|
||||
func FindChecker(id string) *CheckerDefinition {
|
||||
return checkerRegistry[id]
|
||||
}
|
||||
|
||||
// GetObservationProviders returns all registered observation providers.
|
||||
func GetObservationProviders() map[ObservationKey]ObservationProvider {
|
||||
return observationProviderRegistry
|
||||
}
|
||||
|
||||
// FindObservationProvider returns the observation provider for the given key, or nil.
|
||||
func FindObservationProvider(key ObservationKey) ObservationProvider {
|
||||
return observationProviderRegistry[key]
|
||||
}
|
||||
224
checker/server.go
Normal file
224
checker/server.go
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
// Copyright 2020-2026 The happyDomain Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Server is a generic HTTP server for external checkers.
|
||||
// It always exposes /health and /collect. If the provider implements
|
||||
// CheckerDefinitionProvider, it also exposes /definition and /evaluate.
|
||||
// If the provider implements CheckerHTMLReporter or CheckerMetricsReporter,
|
||||
// it also exposes /report.
|
||||
type Server struct {
|
||||
provider ObservationProvider
|
||||
definition *CheckerDefinition
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
// NewServer creates a new checker HTTP server backed by the given provider.
|
||||
// Additional endpoints are registered based on optional interfaces the provider implements.
|
||||
func NewServer(provider ObservationProvider) *Server {
|
||||
s := &Server{provider: provider}
|
||||
s.mux = http.NewServeMux()
|
||||
s.mux.HandleFunc("GET /health", s.handleHealth)
|
||||
s.mux.HandleFunc("POST /collect", s.handleCollect)
|
||||
|
||||
if dp, ok := provider.(CheckerDefinitionProvider); ok {
|
||||
s.definition = dp.Definition()
|
||||
s.definition.BuildRulesInfo()
|
||||
s.mux.HandleFunc("GET /definition", s.handleDefinition)
|
||||
s.mux.HandleFunc("POST /evaluate", s.handleEvaluate)
|
||||
}
|
||||
|
||||
if _, ok := provider.(CheckerHTMLReporter); ok {
|
||||
s.mux.HandleFunc("POST /report", s.handleReport)
|
||||
} else if _, ok := provider.(CheckerMetricsReporter); ok {
|
||||
s.mux.HandleFunc("POST /report", s.handleReport)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Handler returns the http.Handler for this server, allowing callers
|
||||
// to embed it in a custom server or add middleware.
|
||||
func (s *Server) Handler() http.Handler {
|
||||
return requestLogger(s.mux)
|
||||
}
|
||||
|
||||
// ListenAndServe starts the HTTP server on the given address.
|
||||
func (s *Server) ListenAndServe(addr string) error {
|
||||
log.Printf("checker listening on %s", addr)
|
||||
return http.ListenAndServe(addr, requestLogger(s.mux))
|
||||
}
|
||||
|
||||
type statusRecorder struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (r *statusRecorder) WriteHeader(code int) {
|
||||
r.status = code
|
||||
r.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func requestLogger(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
|
||||
next.ServeHTTP(rec, r)
|
||||
log.Printf("%s %s %d %s", r.Method, r.URL.Path, rec.status, time.Since(start))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) handleDefinition(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, s.definition)
|
||||
}
|
||||
|
||||
func (s *Server) handleCollect(w http.ResponseWriter, r *http.Request) {
|
||||
var req ExternalCollectRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, ExternalCollectResponse{
|
||||
Error: fmt.Sprintf("invalid request body: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
data, err := s.provider.Collect(r.Context(), req.Options)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusOK, ExternalCollectResponse{
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusOK, ExternalCollectResponse{
|
||||
Error: fmt.Sprintf("failed to marshal result: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, ExternalCollectResponse{
|
||||
Data: json.RawMessage(raw),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleEvaluate(w http.ResponseWriter, r *http.Request) {
|
||||
var req ExternalEvaluateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, ExternalEvaluateResponse{
|
||||
Error: fmt.Sprintf("invalid request body: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
obs := &mapObservationGetter{data: req.Observations}
|
||||
|
||||
var states []CheckState
|
||||
for _, rule := range s.definition.Rules {
|
||||
if len(req.EnabledRules) > 0 {
|
||||
if enabled, ok := req.EnabledRules[rule.Name()]; ok && !enabled {
|
||||
continue
|
||||
}
|
||||
}
|
||||
state := rule.Evaluate(r.Context(), obs, req.Options)
|
||||
if state.Code == "" {
|
||||
state.Code = rule.Name()
|
||||
}
|
||||
states = append(states, state)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, ExternalEvaluateResponse{States: states})
|
||||
}
|
||||
|
||||
func (s *Server) handleReport(w http.ResponseWriter, r *http.Request) {
|
||||
var req ExternalReportRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": fmt.Sprintf("invalid request body: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
accept := r.Header.Get("Accept")
|
||||
|
||||
if strings.Contains(accept, "text/html") {
|
||||
reporter, ok := s.provider.(CheckerHTMLReporter)
|
||||
if !ok {
|
||||
http.Error(w, "this checker does not support HTML reports", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
html, err := reporter.GetHTMLReport(req.Data)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to generate HTML report: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
|
||||
// Default: JSON metrics.
|
||||
reporter, ok := s.provider.(CheckerMetricsReporter)
|
||||
if !ok {
|
||||
http.Error(w, "this checker does not support metrics reports", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
metrics, err := reporter.ExtractMetrics(req.Data, time.Now())
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
||||
"error": fmt.Sprintf("failed to extract metrics: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, metrics)
|
||||
}
|
||||
|
||||
// mapObservationGetter implements ObservationGetter backed by a static map.
|
||||
type mapObservationGetter struct {
|
||||
data map[ObservationKey]json.RawMessage
|
||||
}
|
||||
|
||||
func (g *mapObservationGetter) Get(ctx context.Context, key ObservationKey, dest any) error {
|
||||
raw, ok := g.data[key]
|
||||
if !ok {
|
||||
return fmt.Errorf("observation %q not available", key)
|
||||
}
|
||||
return json.Unmarshal(raw, dest)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
347
checker/types.go
Normal file
347
checker/types.go
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
// Copyright 2020-2026 The happyDomain Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package checker provides the public types and helpers for writing
|
||||
// happyDomain checker plugins. It is the stable API surface that all
|
||||
// external checkers should depend on.
|
||||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CheckScopeType represents the scope level of a check target.
|
||||
type CheckScopeType int
|
||||
|
||||
const (
|
||||
CheckScopeAdmin CheckScopeType = 0
|
||||
CheckScopeUser CheckScopeType = iota
|
||||
CheckScopeDomain
|
||||
CheckScopeZone
|
||||
CheckScopeService
|
||||
)
|
||||
|
||||
const (
|
||||
AutoFillDomainName = "domain_name"
|
||||
AutoFillSubdomain = "subdomain"
|
||||
AutoFillZone = "zone"
|
||||
AutoFillServiceType = "service_type"
|
||||
AutoFillService = "service"
|
||||
)
|
||||
|
||||
// CheckTarget identifies the resource a check applies to. Identifiers are
|
||||
// passed as opaque strings so the SDK stays self-contained and does not
|
||||
// depend on any happyDomain-specific identifier type. The host is free to
|
||||
// parse them into its own representation at the boundary.
|
||||
type CheckTarget struct {
|
||||
UserId string `json:"userId,omitempty"`
|
||||
DomainId string `json:"domainId,omitempty"`
|
||||
ServiceId string `json:"serviceId,omitempty"`
|
||||
ServiceType string `json:"serviceType,omitempty"`
|
||||
}
|
||||
|
||||
// Scope returns the most specific scope level of this target.
|
||||
func (t CheckTarget) Scope() CheckScopeType {
|
||||
if t.ServiceId != "" {
|
||||
return CheckScopeService
|
||||
}
|
||||
if t.DomainId != "" {
|
||||
return CheckScopeDomain
|
||||
}
|
||||
return CheckScopeUser
|
||||
}
|
||||
|
||||
// String returns a stable string representation of the target.
|
||||
func (t CheckTarget) String() string {
|
||||
var parts []string
|
||||
if t.UserId != "" {
|
||||
parts = append(parts, t.UserId)
|
||||
}
|
||||
if t.DomainId != "" {
|
||||
parts = append(parts, t.DomainId)
|
||||
}
|
||||
if t.ServiceId != "" {
|
||||
parts = append(parts, t.ServiceId)
|
||||
}
|
||||
return strings.Join(parts, "/")
|
||||
}
|
||||
|
||||
// CheckerAvailability declares on which scopes a checker can operate.
|
||||
type CheckerAvailability struct {
|
||||
ApplyToDomain bool `json:"applyToDomain,omitempty"`
|
||||
ApplyToZone bool `json:"applyToZone,omitempty"`
|
||||
ApplyToService bool `json:"applyToService,omitempty"`
|
||||
LimitToProviders []string `json:"limitToProviders,omitempty"`
|
||||
LimitToServices []string `json:"limitToServices,omitempty"`
|
||||
}
|
||||
|
||||
// CheckerOptions holds the runtime options for a checker execution.
|
||||
type CheckerOptions map[string]any
|
||||
|
||||
// CheckerOptionField describes a single checker option, used to document
|
||||
// what configuration the checker accepts. The fields mirror happyDomain's
|
||||
// generic Field type so that the host can re-export it as a type alias and
|
||||
// keep using its existing form-rendering code unchanged.
|
||||
type CheckerOptionField struct {
|
||||
// Id is the option identifier (the key in CheckerOptions).
|
||||
Id string `json:"id" binding:"required"`
|
||||
|
||||
// Type is the string representation of the option's type
|
||||
// (e.g. "string", "number", "uint", "bool").
|
||||
Type string `json:"type" binding:"required"`
|
||||
|
||||
// Label is the title shown to the user.
|
||||
Label string `json:"label,omitempty"`
|
||||
|
||||
// Placeholder is the placeholder shown in the input.
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
|
||||
// Default is the value used when the option is not set by the user.
|
||||
Default any `json:"default,omitempty"`
|
||||
|
||||
// Choices holds the available choices for a dropdown option.
|
||||
Choices []string `json:"choices,omitempty"`
|
||||
|
||||
// Required indicates whether the option must be filled.
|
||||
Required bool `json:"required,omitempty"`
|
||||
|
||||
// Secret indicates that the option holds sensitive information
|
||||
// (API keys, tokens, …).
|
||||
Secret bool `json:"secret,omitempty"`
|
||||
|
||||
// Hide indicates that the option should be hidden from the user.
|
||||
Hide bool `json:"hide,omitempty"`
|
||||
|
||||
// Textarea indicates that a multi-line input should be used.
|
||||
Textarea bool `json:"textarea,omitempty"`
|
||||
|
||||
// Description is a help sentence describing the option.
|
||||
Description string `json:"description,omitempty"`
|
||||
|
||||
// AutoFill indicates that this option is automatically populated by the
|
||||
// host based on execution context (e.g. domain name, service payload).
|
||||
AutoFill string `json:"autoFill,omitempty"`
|
||||
|
||||
// NoOverride indicates that once this option is set at a given scope,
|
||||
// more specific scopes cannot override its value.
|
||||
NoOverride bool `json:"noOverride,omitempty"`
|
||||
}
|
||||
|
||||
// CheckerOptionDocumentation describes a single checker option.
|
||||
type CheckerOptionDocumentation = CheckerOptionField
|
||||
|
||||
// CheckerOptionsDocumentation describes all options a checker accepts, organized by level.
|
||||
type CheckerOptionsDocumentation struct {
|
||||
AdminOpts []CheckerOptionDocumentation `json:"adminOpts,omitempty"`
|
||||
UserOpts []CheckerOptionDocumentation `json:"userOpts,omitempty"`
|
||||
DomainOpts []CheckerOptionDocumentation `json:"domainOpts,omitempty"`
|
||||
ServiceOpts []CheckerOptionDocumentation `json:"serviceOpts,omitempty"`
|
||||
RunOpts []CheckerOptionDocumentation `json:"runOpts,omitempty"`
|
||||
}
|
||||
|
||||
// Status represents the result status of a check evaluation.
|
||||
type Status int
|
||||
|
||||
const (
|
||||
StatusUnknown Status = iota
|
||||
StatusOK
|
||||
StatusInfo
|
||||
StatusWarn
|
||||
StatusCrit
|
||||
StatusError
|
||||
)
|
||||
|
||||
// String returns the human-readable name of the status.
|
||||
func (s Status) String() string {
|
||||
switch s {
|
||||
case StatusUnknown:
|
||||
return "UNKNOWN"
|
||||
case StatusOK:
|
||||
return "OK"
|
||||
case StatusInfo:
|
||||
return "INFO"
|
||||
case StatusWarn:
|
||||
return "WARN"
|
||||
case StatusCrit:
|
||||
return "CRIT"
|
||||
case StatusError:
|
||||
return "ERROR"
|
||||
default:
|
||||
return fmt.Sprintf("Status(%d)", int(s))
|
||||
}
|
||||
}
|
||||
|
||||
// CheckState is the result of evaluating a single rule.
|
||||
type CheckState struct {
|
||||
Status Status `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Meta map[string]any `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// CheckMetric represents a single metric produced by a check.
|
||||
type CheckMetric struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Value float64 `json:"value" binding:"required"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp" binding:"required" format:"date-time"`
|
||||
}
|
||||
|
||||
// ObservationKey identifies a type of observation data.
|
||||
type ObservationKey = string
|
||||
|
||||
// CheckIntervalSpec defines scheduling bounds for a checker.
|
||||
type CheckIntervalSpec struct {
|
||||
Min time.Duration `json:"min" swaggertype:"integer"`
|
||||
Max time.Duration `json:"max" swaggertype:"integer"`
|
||||
Default time.Duration `json:"default" swaggertype:"integer"`
|
||||
}
|
||||
|
||||
// ObservationProvider collects a specific type of data for a target.
|
||||
type ObservationProvider interface {
|
||||
Key() ObservationKey
|
||||
Collect(ctx context.Context, opts CheckerOptions) (any, error)
|
||||
}
|
||||
|
||||
// CheckRuleInfo is the JSON-serializable description of a rule, for API/UI listing.
|
||||
type CheckRuleInfo struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Options *CheckerOptionsDocumentation `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// CheckRule evaluates observations and produces a CheckState.
|
||||
type CheckRule interface {
|
||||
Name() string
|
||||
Description() string
|
||||
Evaluate(ctx context.Context, obs ObservationGetter, opts CheckerOptions) CheckState
|
||||
}
|
||||
|
||||
// CheckRuleWithOptions is an optional interface that rules can implement
|
||||
// to declare their own options documentation for API/UI grouping.
|
||||
type CheckRuleWithOptions interface {
|
||||
CheckRule
|
||||
Options() CheckerOptionsDocumentation
|
||||
}
|
||||
|
||||
// ObservationGetter provides access to observation data (used by CheckRule).
|
||||
// Get unmarshals observation data into dest (like json.Unmarshal).
|
||||
type ObservationGetter interface {
|
||||
Get(ctx context.Context, key ObservationKey, dest any) error
|
||||
}
|
||||
|
||||
// CheckAggregator combines multiple CheckStates into a single result.
|
||||
type CheckAggregator interface {
|
||||
Aggregate(states []CheckState) CheckState
|
||||
}
|
||||
|
||||
// CheckerHTMLReporter is an optional interface that observation providers can
|
||||
// implement to render their stored data as a full HTML document (for iframe embedding).
|
||||
// Detect support with a type assertion: _, ok := provider.(CheckerHTMLReporter)
|
||||
type CheckerHTMLReporter interface {
|
||||
// GetHTMLReport generates an HTML document from the JSON-encoded observation data.
|
||||
GetHTMLReport(raw json.RawMessage) (string, error)
|
||||
}
|
||||
|
||||
// CheckerMetricsReporter is an optional interface that observation providers can
|
||||
// implement to extract time-series metrics from their stored data.
|
||||
// Detect support with a type assertion: _, ok := provider.(CheckerMetricsReporter)
|
||||
type CheckerMetricsReporter interface {
|
||||
// ExtractMetrics returns metrics from JSON-encoded observation data.
|
||||
ExtractMetrics(raw json.RawMessage, collectedAt time.Time) ([]CheckMetric, error)
|
||||
}
|
||||
|
||||
// CheckerDefinitionProvider is an optional interface that observation providers can
|
||||
// implement to expose their checker definition. Used by the SDK server to serve
|
||||
// /definition and /evaluate endpoints without requiring a separate argument.
|
||||
// Detect support with a type assertion: _, ok := provider.(CheckerDefinitionProvider)
|
||||
type CheckerDefinitionProvider interface {
|
||||
// Definition returns the checker definition for this provider.
|
||||
Definition() *CheckerDefinition
|
||||
}
|
||||
|
||||
// CheckerDefinition is the complete definition of a checker, registered via init().
|
||||
type CheckerDefinition struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Availability CheckerAvailability `json:"availability"`
|
||||
Options CheckerOptionsDocumentation `json:"options"`
|
||||
RulesInfo []CheckRuleInfo `json:"rules"`
|
||||
Rules []CheckRule `json:"-"`
|
||||
Aggregator CheckAggregator `json:"-"`
|
||||
Interval *CheckIntervalSpec `json:"interval,omitempty"`
|
||||
HasHTMLReport bool `json:"has_html_report,omitempty"`
|
||||
HasMetrics bool `json:"has_metrics,omitempty"`
|
||||
ObservationKeys []ObservationKey `json:"observationKeys,omitempty"`
|
||||
}
|
||||
|
||||
// BuildRulesInfo populates RulesInfo from the Rules slice.
|
||||
func (d *CheckerDefinition) BuildRulesInfo() {
|
||||
d.RulesInfo = make([]CheckRuleInfo, len(d.Rules))
|
||||
for i, rule := range d.Rules {
|
||||
info := CheckRuleInfo{
|
||||
Name: rule.Name(),
|
||||
Description: rule.Description(),
|
||||
}
|
||||
if rwo, ok := rule.(CheckRuleWithOptions); ok {
|
||||
opts := rwo.Options()
|
||||
info.Options = &opts
|
||||
}
|
||||
d.RulesInfo[i] = info
|
||||
}
|
||||
}
|
||||
|
||||
// OptionsValidator is an optional interface that checkers (or their rules/providers)
|
||||
// can implement to perform domain-specific validation of checker options.
|
||||
type OptionsValidator interface {
|
||||
ValidateOptions(opts CheckerOptions) error
|
||||
}
|
||||
|
||||
// ExternalCollectRequest is sent to POST /collect on a remote checker endpoint.
|
||||
type ExternalCollectRequest struct {
|
||||
Key ObservationKey `json:"key"`
|
||||
Target CheckTarget `json:"target"`
|
||||
Options CheckerOptions `json:"options"`
|
||||
}
|
||||
|
||||
// ExternalCollectResponse is returned by POST /collect on a remote checker endpoint.
|
||||
type ExternalCollectResponse struct {
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ExternalEvaluateRequest is sent to POST /evaluate on a remote checker endpoint.
|
||||
type ExternalEvaluateRequest struct {
|
||||
Observations map[ObservationKey]json.RawMessage `json:"observations"`
|
||||
Options CheckerOptions `json:"options"`
|
||||
EnabledRules map[string]bool `json:"enabledRules,omitempty"`
|
||||
}
|
||||
|
||||
// ExternalEvaluateResponse is returned by POST /evaluate on a remote checker endpoint.
|
||||
type ExternalEvaluateResponse struct {
|
||||
States []CheckState `json:"states"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ExternalReportRequest is sent to POST /report on a remote checker endpoint.
|
||||
type ExternalReportRequest struct {
|
||||
Key ObservationKey `json:"key"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue