Initial commit

This commit is contained in:
nemunaire 2026-04-07 20:37:44 +07:00
commit 8160adcdca
8 changed files with 990 additions and 0 deletions

202
LICENSE Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
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.

10
NOTICE Normal file
View file

@ -0,0 +1,10 @@
checker-sdk-go
Copyright 2020-2026 The happyDomain Authors
This product includes software developed as part of the happyDomain project
(https://happydomain.org).
Portions of this code were originally written for the happyDomain server
(licensed under AGPL-3.0 and a commercial license) and are made available
here under the Apache License, Version 2.0 to enable a permissively licensed
ecosystem of checker plugins.

35
README.md Normal file
View file

@ -0,0 +1,35 @@
# checker-sdk-go
Public Go SDK for writing [happyDomain](https://happydomain.org) checker plugins.
This module provides the stable types, helpers, and HTTP server scaffolding
that all checker plugins need, independent of the happyDomain server itself.
## Why a separate module?
The happyDomain server is licensed under AGPL-3.0 (with a commercial option).
If checkers had to import the server's internal packages, every checker, even
trivial ones, would inherit those licensing constraints.
This SDK is released under the **Apache License 2.0**, so:
- You can write a checker plugin under any license you want (MIT, Apache,
proprietary, AGPL, whatever fits your needs).
- A plugin built against this SDK is *not* a derivative work of happyDomain.
- happyDomain itself depends on this SDK (as an Apache-licensed dependency,
which is compatible with AGPL).
## Installation
```bash
go get git.happydns.org/checker-sdk-go/checker
```
## Getting started
See [checker-dummy](https://git.happydns.org/checker-dummy) for a
fully working, documented template.
## License
Apache License 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).

91
checker/options.go Normal file
View 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
View 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
View 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
View 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"`
}

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module git.happydns.org/checker-sdk-go
go 1.25.0