Initial commit

This commit is contained in:
nemunaire 2026-04-07 14:55:34 +07:00
commit 2ee9e93bf6
15 changed files with 1047 additions and 0 deletions

46
checker/collect.go Normal file
View file

@ -0,0 +1,46 @@
package checker
import (
"context"
"math/rand/v2"
"time"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Collect gathers observation data. This is called by happyDomain (or the
// /collect HTTP endpoint) every time a check runs.
//
// In a real checker, this is where you would perform the actual monitoring
// work: sending network requests, querying APIs, measuring latency, etc.
//
// This dummy implementation simply reads options and generates a random score
// so you can focus on the structure rather than external dependencies.
//
// Parameters:
// - ctx: a context for cancellation/timeout - always honour it.
// - opts: the merged checker options (admin + user + domain + service + run).
//
// Return:
// - any: the observation data (will be JSON-serialised by the SDK).
// - error: non-nil if collection failed entirely.
func (p *dummyProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
// Read user-configurable options using the SDK helpers.
// These helpers handle type coercion gracefully - the value may come as
// a native Go type (in-process plugin) or as a JSON-decoded float64/string
// (external HTTP mode). The helpers normalise both cases.
message := "Hello from the dummy checker!"
if v, ok := sdk.GetOption[string](opts, "message"); ok && v != "" {
message = v
}
// Generate a random score between 0 and 100 to simulate a measurement.
// In your real checker, replace this with actual monitoring logic.
score := rand.Float64() * 100
return &DummyData{
Message: message,
Score: score,
CollectedAt: time.Now(),
}, nil
}

116
checker/definition.go Normal file
View file

@ -0,0 +1,116 @@
package checker
import (
"time"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Version is the checker version reported in CheckerDefinition.Version.
//
// It defaults to "built-in", which is appropriate when the checker package is
// imported directly (built-in or plugin mode). Standalone binaries (like
// main.go) should override this from their own Version variable at the start
// of main(), which makes it easy for CI to inject a version with a single
// -ldflags "-X main.Version=..." flag instead of targeting the nested
// package path.
var Version = "built-in"
// Definition returns the CheckerDefinition for the dummy checker.
//
// A CheckerDefinition tells happyDomain everything it needs to know about
// your checker: its identity, where it can be applied, what options it
// accepts, what rules it provides, and how often it should run.
func Definition() *sdk.CheckerDefinition {
return &sdk.CheckerDefinition{
// ID is a unique, stable identifier for this checker. It is stored in
// the database, so never change it after release.
ID: "dummy",
// Name is the human-readable label shown in the happyDomain UI.
Name: "Dummy (example)",
// Version is an optional version string for this checker. It is
// surfaced in the UI/API and is useful to track which iteration of
// your checker produced a given observation. The value is injected
// at build time via -ldflags "-X .../checker.Version=...".
Version: Version,
// Availability controls where this checker appears in the UI.
// A checker can apply at the domain level, zone level, or service
// level. You can also restrict it to specific service types.
//
// Here we apply it at the domain level, which means users will see
// this checker in the "Domain checks" section and it does not require
// a specific service to be present.
Availability: sdk.CheckerAvailability{
ApplyToDomain: true,
},
// ObservationKeys lists the keys this checker produces. This ties
// the definition to the provider(s) that generate the data.
ObservationKeys: []sdk.ObservationKey{ObservationKeyDummy},
// Options documents what configuration the checker accepts. Options
// are grouped by audience (admin, user, domain, service, run):
//
// - AdminOpts: set once by the happyDomain administrator
// - UserOpts: editable by end-users in the checker settings UI
// - DomainOpts: auto-filled per domain (domain_name, etc.)
// - ServiceOpts: auto-filled per service (the service payload)
// - RunOpts: set at collect-time only (e.g., overrides)
//
// Each option has an Id (used as the key in CheckerOptions), a Type
// for the UI widget, a Label, and optionally a Default value.
Options: sdk.CheckerOptionsDocumentation{
UserOpts: []sdk.CheckerOptionDocumentation{
{
Id: "message",
Type: "string",
Label: "Custom message",
Description: "A message that will be included in the observation data.",
Default: "Hello from the dummy checker!",
},
{
Id: "warningThreshold",
Type: "number",
Label: "Warning threshold (score)",
Description: "If the score drops below this value, the check status becomes Warning.",
Default: float64(50),
},
{
Id: "criticalThreshold",
Type: "number",
Label: "Critical threshold (score)",
Description: "If the score drops below this value, the check status becomes Critical.",
Default: float64(20),
},
},
DomainOpts: []sdk.CheckerOptionDocumentation{
{
Id: "domain_name",
Label: "Domain name",
AutoFill: sdk.AutoFillDomainName,
},
},
},
// Rules lists the evaluation rules provided by this checker. Each
// rule will appear in the UI, and users can enable/disable them
// individually.
Rules: []sdk.CheckRule{
Rule(),
},
// Interval specifies how often the check should run.
Interval: &sdk.CheckIntervalSpec{
Min: 1 * time.Minute,
Max: 1 * time.Hour,
Default: 5 * time.Minute,
},
// HasMetrics indicates that this checker can produce time-series
// metrics (because our provider implements CheckerMetricsReporter).
HasMetrics: true,
}
}

61
checker/provider.go Normal file
View file

@ -0,0 +1,61 @@
package checker
import (
"encoding/json"
"time"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Provider returns a new dummy observation provider.
//
// The provider is the central object of a checker. It implements the
// ObservationProvider interface (required) and can optionally implement
// additional interfaces to unlock more features:
//
// - CheckerDefinitionProvider → exposes /definition and /evaluate endpoints
// - CheckerMetricsReporter → exposes /report (JSON metrics) endpoint
// - CheckerHTMLReporter → exposes /report (HTML) endpoint
//
// In this example, the provider implements all three optional interfaces
// so you can see how each one works.
func Provider() sdk.ObservationProvider {
return &dummyProvider{}
}
// dummyProvider is the concrete type that satisfies the ObservationProvider
// interface and the optional reporter interfaces.
type dummyProvider struct{}
// Key returns the observation key for this provider. This must match the key
// used in your CheckerDefinition's ObservationKeys list so happyDomain knows
// which provider produces which data.
func (p *dummyProvider) Key() sdk.ObservationKey {
return ObservationKeyDummy
}
// Definition implements sdk.CheckerDefinitionProvider.
// Returning a definition enables the /definition and /evaluate HTTP endpoints
// in the SDK server, and lets happyDomain discover this checker's metadata.
func (p *dummyProvider) Definition() *sdk.CheckerDefinition {
return Definition()
}
// ExtractMetrics implements sdk.CheckerMetricsReporter.
// This is called when happyDomain (or the /report endpoint) needs to turn
// raw observation data into time-series metrics for graphing.
func (p *dummyProvider) ExtractMetrics(raw json.RawMessage, collectedAt time.Time) ([]sdk.CheckMetric, error) {
var data DummyData
if err := json.Unmarshal(raw, &data); err != nil {
return nil, err
}
return []sdk.CheckMetric{
{
Name: "dummy_score",
Value: data.Score,
Unit: "points",
Timestamp: collectedAt,
},
}, nil
}

96
checker/rule.go Normal file
View file

@ -0,0 +1,96 @@
package checker
import (
"context"
"fmt"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Rule returns a new dummy check rule.
//
// A rule evaluates collected observation data and returns a status (OK,
// Warning, Critical, Error). Each checker can define multiple rules that
// inspect the same data from different angles.
func Rule() sdk.CheckRule {
return &dummyRule{}
}
// dummyRule implements the sdk.CheckRule interface.
type dummyRule struct{}
// Name returns a unique, stable identifier for this rule. It is used as the
// "code" field in check results and stored in the database.
func (r *dummyRule) Name() string { return "dummy_score_check" }
// Description returns a human-readable summary of what this rule checks.
func (r *dummyRule) Description() string {
return "Checks whether the dummy score is above the configured thresholds"
}
// ValidateOptions is called before evaluation to verify that the options are
// well-formed. Return an error to reject invalid configuration early, before
// any data collection happens.
func (r *dummyRule) ValidateOptions(opts sdk.CheckerOptions) error {
warning := sdk.GetFloatOption(opts, "warningThreshold", 50)
critical := sdk.GetFloatOption(opts, "criticalThreshold", 20)
if warning < 0 || warning > 100 {
return fmt.Errorf("warningThreshold must be between 0 and 100")
}
if critical < 0 || critical > 100 {
return fmt.Errorf("criticalThreshold must be between 0 and 100")
}
if critical >= warning {
return fmt.Errorf("criticalThreshold (%v) must be less than warningThreshold (%v)", critical, warning)
}
return nil
}
// Evaluate inspects the collected observation data and returns a CheckState.
//
// Parameters:
// - ctx: context for cancellation.
// - obs: an ObservationGetter to retrieve collected data by key.
// - opts: the merged checker options.
//
// The ObservationGetter.Get method deserialises the stored JSON into your data
// struct. Always check the error: the observation may not be available if
// collection failed.
func (r *dummyRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) sdk.CheckState {
// Retrieve the observation data by key.
var data DummyData
if err := obs.Get(ctx, ObservationKeyDummy, &data); err != nil {
return sdk.CheckState{
Status: sdk.StatusError,
Message: fmt.Sprintf("Failed to get dummy data: %v", err),
Code: "dummy_error",
}
}
// Read thresholds from options.
warningThreshold := sdk.GetFloatOption(opts, "warningThreshold", 50)
criticalThreshold := sdk.GetFloatOption(opts, "criticalThreshold", 20)
// Determine the status based on the score and thresholds.
var status sdk.Status
switch {
case data.Score < criticalThreshold:
status = sdk.StatusCrit
case data.Score < warningThreshold:
status = sdk.StatusWarn
default:
status = sdk.StatusOK
}
return sdk.CheckState{
Status: status,
Message: fmt.Sprintf("Score: %.1f - %s", data.Score, data.Message),
Code: "dummy_score_check",
Meta: map[string]any{
"score": data.Score,
"message": data.Message,
},
}
}

33
checker/types.go Normal file
View file

@ -0,0 +1,33 @@
// Package checker implements a dummy checker for happyDomain.
//
// This is an educational example that demonstrates all the building blocks
// needed to create a happyDomain checker. It performs no real monitoring;
// instead, it returns a configurable message and a random score, so you can
// focus on the structure without worrying about external dependencies.
package checker
import "time"
// ObservationKeyDummy is the unique key that identifies observations
// produced by this checker. Every checker must define at least one key so
// happyDomain can store and retrieve its data.
const ObservationKeyDummy = "dummy"
// DummyData is the data structure returned by Collect.
//
// When happyDomain collects an observation, it serialises this struct to JSON
// and stores it. Later, during evaluation, the same JSON is deserialised back
// into this struct. Design this type to hold everything your rules will need
// to decide OK / Warning / Critical.
type DummyData struct {
// Message is an arbitrary string returned as part of the observation.
Message string `json:"message"`
// Score is a number between 0 and 100. The evaluation rules compare it
// against user-defined thresholds to determine the check status.
Score float64 `json:"score"`
// CollectedAt records when the observation was taken. It is used by the
// metrics reporter to timestamp the extracted metrics.
CollectedAt time.Time `json:"collected_at"`
}