Initial commit
This commit is contained in:
commit
52c776a3ff
14 changed files with 1156 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
checker-dummy
|
||||
*.so
|
||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
ARG CHECKER_VERSION=custom-build
|
||||
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-X main.Version=${CHECKER_VERSION}" -o /checker-dummy .
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /checker-dummy /checker-dummy
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/checker-dummy"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 The happyDomain Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the “Software”), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
25
Makefile
Normal file
25
Makefile
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
CHECKER_NAME := checker-dummy
|
||||
CHECKER_IMAGE := happydomain/$(CHECKER_NAME)
|
||||
CHECKER_VERSION ?= custom-build
|
||||
|
||||
CHECKER_SOURCES := main.go $(wildcard checker/*.go)
|
||||
|
||||
GO_LDFLAGS := -X main.Version=$(CHECKER_VERSION)
|
||||
|
||||
.PHONY: all plugin docker clean
|
||||
|
||||
all: $(CHECKER_NAME)
|
||||
|
||||
$(CHECKER_NAME): $(CHECKER_SOURCES)
|
||||
go build -ldflags "$(GO_LDFLAGS)" -o $@ .
|
||||
|
||||
plugin: $(CHECKER_NAME).so
|
||||
|
||||
$(CHECKER_NAME).so: $(CHECKER_SOURCES) $(wildcard plugin/*.go)
|
||||
go build -buildmode=plugin -ldflags "$(GO_LDFLAGS)" -o $@ ./plugin/
|
||||
|
||||
docker:
|
||||
docker build --build-arg CHECKER_VERSION=$(CHECKER_VERSION) -t $(CHECKER_IMAGE) .
|
||||
|
||||
clean:
|
||||
rm -f $(CHECKER_NAME) $(CHECKER_NAME).so
|
||||
542
README.md
Normal file
542
README.md
Normal file
|
|
@ -0,0 +1,542 @@
|
|||
# checker-dummy - How to Build a happyDomain Checker
|
||||
|
||||
This repository is a **fully working, educational example** of a happyDomain checker. It is intentionally simple: instead of performing real monitoring, it returns a random score and a user-configurable message. This lets you focus on learning the structure without dealing with external dependencies.
|
||||
|
||||
Use this as a template when you create your own checker.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [What is a Checker?](#what-is-a-checker)
|
||||
2. [Architecture Overview](#architecture-overview)
|
||||
3. [Repository Structure](#repository-structure)
|
||||
4. [Step-by-Step Walkthrough](#step-by-step-walkthrough)
|
||||
- [Step 1: Define Your Data Types](#step-1-define-your-data-types)
|
||||
- [Step 2: Create the Provider](#step-2-create-the-provider)
|
||||
- [Step 3: Implement Data Collection](#step-3-implement-data-collection)
|
||||
- [Step 4: Describe Your Checker (Definition)](#step-4-describe-your-checker-definition)
|
||||
- [Step 5: Write Evaluation Rules](#step-5-write-evaluation-rules)
|
||||
- [Step 6: Wire It Up (main.go)](#step-6-wire-it-up-maingo)
|
||||
- [Step 7: Create the Plugin Entrypoint](#step-7-create-the-plugin-entrypoint)
|
||||
5. [Running the Checker](#running-the-checker)
|
||||
6. [Testing with curl](#testing-with-curl)
|
||||
7. [Deploying to happyDomain](#deploying-to-happydomain)
|
||||
8. [License & happyDomain compatibility](#license--happydomain-compatibility)
|
||||
9. [Going Further](#going-further)
|
||||
|
||||
---
|
||||
|
||||
## What is a Checker?
|
||||
|
||||
A **checker** is a small, self-contained program that monitors one aspect of a domain's DNS infrastructure. happyDomain runs checkers periodically and displays their results in its dashboard.
|
||||
|
||||
Every checker does three things:
|
||||
|
||||
1. **Collect:** Gather raw observation data (e.g., ping a server, query an API, measure DNS response time).
|
||||
2. **Evaluate:** Compare the collected data against user-defined thresholds to produce a status: OK, Warning, or Critical.
|
||||
3. **Report** *(optional)*: Extract time-series metrics or generate HTML reports for the dashboard.
|
||||
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
A checker can run in two modes:
|
||||
|
||||
### Standalone HTTP Server (External Checker)
|
||||
|
||||
The checker runs as its own process and exposes an HTTP API. happyDomain communicates with it over the network. This is the most flexible option: you can write your checker in any language, deploy it independently, and scale it separately.
|
||||
|
||||
```
|
||||
┌─────────────┐ HTTP ┌─────────────────┐
|
||||
│ happyDomain │ ──────────► │ checker-dummy │
|
||||
│ server │ ◄────────── │ (this program) │
|
||||
└─────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### In-Process Plugin
|
||||
|
||||
The checker is compiled as a Go plugin (`.so` file) and loaded directly into the happyDomain process. This is simpler to deploy (single binary) but requires the checker to be written in Go.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ happyDomain server │
|
||||
│ │
|
||||
│ ┌──────────────────────────────┐ │
|
||||
│ │ checker-dummy.so (plugin) │ │
|
||||
│ │ checker-ping.so (plugin) │ │
|
||||
│ │ checker-matrix.so (plugin) │ │
|
||||
│ │ checker-....so (plugin) │ │
|
||||
│ └──────────────────────────────┘ │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Built-in Checker
|
||||
|
||||
The checker package can be imported directly into the happyDomain server and registered at init time: no plugin loading, no separate process. This avoids the operational burden of Go's plugin system (matching toolchain versions, CGO, `.so` distribution) entirely.
|
||||
|
||||
```go
|
||||
// happydomain/checkers/ping.go
|
||||
package checkers
|
||||
|
||||
import (
|
||||
ping "git.happydns.org/happyDomain/checker-ping/checker"
|
||||
"git.happydns.org/happyDomain/internal/checker"
|
||||
)
|
||||
|
||||
func init() {
|
||||
checker.RegisterObservationProvider(ping.Provider())
|
||||
checker.RegisterExternalizableChecker(ping.Definition())
|
||||
}
|
||||
```
|
||||
|
||||
This mode is reserved for checkers maintained as part of the happyDomain project itself. Or if you compile yourself your own version of happyDomain.
|
||||
|
||||
**Both standalone, plugin and built-in modes use the same checker code; only the entry point differs.**
|
||||
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
checker-dummy/
|
||||
├── main.go # Entry point for standalone HTTP server mode
|
||||
├── checker/
|
||||
│ ├── types.go # Data structures (what the checker observes)
|
||||
│ ├── provider.go # The provider: glues everything together
|
||||
│ ├── collect.go # Collection logic (the actual monitoring)
|
||||
│ ├── definition.go # Checker metadata (options, rules, intervals)
|
||||
│ └── rule.go # Evaluation rules (OK / Warning / Critical)
|
||||
├── plugin/
|
||||
│ └── plugin.go # Entry point for plugin mode
|
||||
├── go.mod # Go module definition
|
||||
├── Makefile # Build targets
|
||||
├── Dockerfile # Container image
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
Each file has a single, clear responsibility. This is the recommended layout for all happyDomain checkers.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Step-by-Step Walkthrough
|
||||
|
||||
### Step 1: Define Your Data Types
|
||||
|
||||
**File: `checker/types.go`**
|
||||
|
||||
Start by defining the data structure that your checker will produce during collection. This struct is serialised to JSON by the SDK, stored by happyDomain, and later deserialised during evaluation.
|
||||
|
||||
```go
|
||||
const ObservationKeyDummy = "dummy"
|
||||
|
||||
type DummyData struct {
|
||||
Message string `json:"message"`
|
||||
Score float64 `json:"score"`
|
||||
CollectedAt time.Time `json:"collected_at"`
|
||||
}
|
||||
```
|
||||
|
||||
Key points:
|
||||
- **`ObservationKeyDummy`** is a unique string that identifies observations produced by this checker. Every checker needs at least one key.
|
||||
- **Design for evaluation**: include everything your rules will need to decide OK/Warning/Critical. The evaluation step only sees this struct; it cannot re-collect data.
|
||||
|
||||
### Step 2: Create the Provider
|
||||
|
||||
**File: `checker/provider.go`**
|
||||
|
||||
The **provider** is the central object of your checker. It must implement the `ObservationProvider` interface:
|
||||
|
||||
```go
|
||||
type ObservationProvider interface {
|
||||
Key() ObservationKey
|
||||
Collect(ctx context.Context, opts CheckerOptions) (any, error)
|
||||
}
|
||||
```
|
||||
|
||||
You can also implement optional interfaces to unlock additional features:
|
||||
|
||||
| Interface | What it enables |
|
||||
|-------------------------------|------------------------------------------|
|
||||
| `CheckerDefinitionProvider` | `/definition` and `/evaluate` endpoints |
|
||||
| `CheckerMetricsReporter` | `/report` endpoint (JSON metrics) |
|
||||
| `CheckerHTMLReporter` | `/report` endpoint (HTML) |
|
||||
|
||||
In this example, we implement all three optional interfaces:
|
||||
|
||||
```go
|
||||
type dummyProvider struct{}
|
||||
|
||||
func (p *dummyProvider) Key() ObservationKey { return ObservationKeyDummy }
|
||||
func (p *dummyProvider) Definition() *CheckerDefinition { return Definition() }
|
||||
func (p *dummyProvider) ExtractMetrics(raw json.RawMessage, collectedAt time.Time) ([]CheckMetric, error) { ... }
|
||||
```
|
||||
|
||||
The `Key()` method must return the same string as your `ObservationKeyDummy` constant.
|
||||
|
||||
### Step 3: Implement Data Collection
|
||||
|
||||
**File: `checker/collect.go`**
|
||||
|
||||
This is where the real work happens. The `Collect` method is called every time happyDomain runs your check.
|
||||
|
||||
```go
|
||||
func (p *dummyProvider) Collect(ctx context.Context, opts CheckerOptions) (any, error) {
|
||||
// Read options using SDK helpers
|
||||
message := "Hello from the dummy checker!"
|
||||
if v, ok := sdk.GetOption[string](opts, "message"); ok && v != "" {
|
||||
message = v
|
||||
}
|
||||
|
||||
// Do your monitoring work here!
|
||||
// In a real checker, you would: ping a server, query an API,
|
||||
// measure DNS response time, check TLS certificates, etc.
|
||||
score := rand.Float64() * 100
|
||||
|
||||
return &DummyData{
|
||||
Message: message,
|
||||
Score: score,
|
||||
CollectedAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
Key points:
|
||||
- **Always honour `ctx`**: happyDomain may cancel long-running checks.
|
||||
- **Use SDK option helpers** (`sdk.GetOption`, `sdk.GetFloatOption`, `sdk.GetIntOption`, `sdk.GetBoolOption`) to read options. They handle type coercion between in-process (native Go types) and HTTP mode (JSON-decoded types).
|
||||
- **Return your data struct**: the SDK serialises it to JSON automatically.
|
||||
- **Return an error** only if collection failed entirely. Partial results are fine.
|
||||
|
||||
### Step 4: Describe Your Checker (Definition)
|
||||
|
||||
**File: `checker/definition.go`**
|
||||
|
||||
The `CheckerDefinition` tells happyDomain everything about your checker:
|
||||
|
||||
```go
|
||||
func Definition() *CheckerDefinition {
|
||||
return &CheckerDefinition{
|
||||
ID: "dummy", // Unique, stable identifier (never change after release)
|
||||
Name: "Dummy (example)", // Human-readable label for the UI
|
||||
Version: Version, // Optional; injected at build time via -ldflags
|
||||
|
||||
Availability: CheckerAvailability{
|
||||
ApplyToDomain: true, // Show in the "Domain checks" section
|
||||
},
|
||||
|
||||
ObservationKeys: []ObservationKey{ObservationKeyDummy},
|
||||
|
||||
Options: CheckerOptionsDocumentation{
|
||||
UserOpts: []CheckerOptionDocumentation{
|
||||
{Id: "message", Type: "string", Label: "Custom message", Default: "Hello!"},
|
||||
{Id: "warningThreshold", Type: "number", Label: "Warning threshold", Default: float64(50)},
|
||||
...
|
||||
},
|
||||
},
|
||||
|
||||
Rules: []CheckRule{Rule()},
|
||||
|
||||
Interval: &CheckIntervalSpec{
|
||||
Min: 1 * time.Minute, Max: 1 * time.Hour, Default: 5 * time.Minute,
|
||||
},
|
||||
|
||||
HasMetrics: true,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Version**: declare a package-level `var Version = "built-in"` in your checker package and reference it from the definition. The default is fine when the package is imported directly (built-in or plugin mode). For the standalone binary, declare a separate `var Version = "custom-build"` in `main.go` and propagate it in `init()`:
|
||||
|
||||
```go
|
||||
// main.go
|
||||
var Version = "custom-build"
|
||||
|
||||
func init() {
|
||||
dummy.Version = Version
|
||||
}
|
||||
```
|
||||
|
||||
The CI can then override the standalone binary's version with a simple flag, without having to know the nested package path:
|
||||
|
||||
```bash
|
||||
go build -ldflags "-X main.Version=$(git describe --tags)" .
|
||||
```
|
||||
|
||||
**Availability**: choose where your checker appears:
|
||||
|
||||
| Field | When to use |
|
||||
|------------------|-----------------------------------------------------|
|
||||
| `ApplyToDomain` | The check applies to the entire domain |
|
||||
| `ApplyToZone` | The check applies to a specific DNS zone |
|
||||
| `ApplyToService` | The check applies to a specific service (e.g., A/AAAA records). Use `LimitToServices` to restrict which service types. |
|
||||
|
||||
**Options**: grouped by audience:
|
||||
|
||||
| Group | Who sets it | Example |
|
||||
|---------------|----------------------|--------------------------------------|
|
||||
| `AdminOpts` | happyDomain admin | API endpoint URL |
|
||||
| `UserOpts` | End-user in the UI | Thresholds, count, custom messages |
|
||||
| `DomainOpts` | Auto-filled per domain | `domain_name` (via `AutoFill`) |
|
||||
| `ServiceOpts` | Auto-filled per service | The service payload (via `AutoFill`) |
|
||||
| `RunOpts` | Set at collect-time | Runtime overrides |
|
||||
|
||||
**Option types** for the UI widget: `"string"`, `"number"`, `"uint"`, `"bool"`. You can also provide `Choices` for dropdown menus.
|
||||
|
||||
### Step 5: Write Evaluation Rules
|
||||
|
||||
**File: `checker/rule.go`**
|
||||
|
||||
A rule implements the `CheckRule` interface:
|
||||
|
||||
```go
|
||||
type CheckRule interface {
|
||||
Name() string
|
||||
Description() string
|
||||
Evaluate(ctx context.Context, obs ObservationGetter, opts CheckerOptions) CheckState
|
||||
}
|
||||
```
|
||||
|
||||
Optionally, your rule can also implement `ValidateOptions(opts) error` for early validation.
|
||||
|
||||
The `Evaluate` method receives an `ObservationGetter` to retrieve the collected data:
|
||||
|
||||
```go
|
||||
func (r *dummyRule) Evaluate(ctx context.Context, obs ObservationGetter, opts CheckerOptions) CheckState {
|
||||
var data DummyData
|
||||
if err := obs.Get(ctx, ObservationKeyDummy, &data); err != nil {
|
||||
return CheckState{Status: StatusError, Message: "..."}
|
||||
}
|
||||
|
||||
warningThreshold := sdk.GetFloatOption(opts, "warningThreshold", 50)
|
||||
criticalThreshold := sdk.GetFloatOption(opts, "criticalThreshold", 20)
|
||||
|
||||
switch {
|
||||
case data.Score < criticalThreshold:
|
||||
return CheckState{Status: StatusCrit, ...}
|
||||
case data.Score < warningThreshold:
|
||||
return CheckState{Status: StatusWarn, ...}
|
||||
default:
|
||||
return CheckState{Status: StatusOK, ...}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Status values**: `StatusOK`, `StatusWarn`, `StatusCrit`, `StatusError`, `StatusUnknown`.
|
||||
|
||||
You can define **multiple rules** per checker. Each rule evaluates the same collected data from a different angle. Users can enable/disable rules individually in the UI.
|
||||
|
||||
### Step 6: Wire It Up (main.go)
|
||||
|
||||
**File: `main.go`**
|
||||
|
||||
The standalone entry point is minimal; the SDK does all the heavy lifting:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
flag.Parse()
|
||||
server := sdk.NewServer(dummy.Provider())
|
||||
server.ListenAndServe(*listenAddr)
|
||||
}
|
||||
```
|
||||
|
||||
`sdk.NewServer` inspects your provider and automatically registers HTTP endpoints based on which interfaces it implements:
|
||||
|
||||
| Endpoint | Always | Requires |
|
||||
|--------------------|--------|------------------------------|
|
||||
| `GET /health` | Yes | - |
|
||||
| `POST /collect` | Yes | - |
|
||||
| `GET /definition` | - | `CheckerDefinitionProvider` |
|
||||
| `POST /evaluate` | - | `CheckerDefinitionProvider` |
|
||||
| `POST /report` | - | `CheckerMetricsReporter` or `CheckerHTMLReporter` |
|
||||
|
||||
### Step 7: Create the Plugin Entrypoint
|
||||
|
||||
**File: `plugin/plugin.go`**
|
||||
|
||||
For in-process plugin mode, the entrypoint must be a `package main` that exposes a `NewCheckerPlugin` symbol. happyDomain opens the `.so` file with `plugin.Open`, looks up that symbol, and calls it to obtain the checker definition and its observation provider — which the host then registers in its own global registries.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
dummy "git.happydns.org/happyDomain/checker-dummy/checker"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
func NewCheckerPlugin() (*happydns.CheckerDefinition, happydns.ObservationProvider, error) {
|
||||
return dummy.Definition(), dummy.Provider(), nil
|
||||
}
|
||||
|
||||
func main() {}
|
||||
```
|
||||
|
||||
A few constraints to keep in mind:
|
||||
|
||||
- **`package main` is mandatory**: `go build -buildmode=plugin` refuses anything else, even though no `main()` is ever executed (an empty one is still required to satisfy the compiler).
|
||||
- **Don't register from `init()`**: it would only work if the host and the plugin were built against the exact same versions of every shared dependency. The explicit factory symbol makes the contract clear and survives dependency drift much better.
|
||||
- **The factory signature is the contract** between the plugin and the host. If happyDomain changes it, every plugin must be rebuilt.
|
||||
|
||||
Build the plugin with:
|
||||
|
||||
```bash
|
||||
go build -buildmode=plugin -o checker-dummy.so ./plugin
|
||||
```
|
||||
|
||||
Then drop the resulting `checker-dummy.so` into one of happyDomain's configured plugin directories — it will be picked up at startup.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Running the Checker
|
||||
|
||||
### Build and run locally
|
||||
|
||||
```bash
|
||||
make build
|
||||
./checker-dummy -listen :8080
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
make docker
|
||||
docker run -p 8080:8080 happydomain/checker-dummy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Testing with curl
|
||||
|
||||
### Health check
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
# {"status":"ok"}
|
||||
```
|
||||
|
||||
### Get the checker definition
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/definition
|
||||
```
|
||||
|
||||
### Collect an observation
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/collect \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"key": "dummy",
|
||||
"options": {
|
||||
"message": "Testing my checker!"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"message": "Testing my checker!",
|
||||
"score": 73.2,
|
||||
"collected_at": "2026-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Evaluate observations
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/evaluate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"observations": {
|
||||
"dummy": "{\"message\":\"test\",\"score\":42.5,\"collected_at\":\"2026-01-15T10:30:00Z\"}"
|
||||
},
|
||||
"options": {
|
||||
"warningThreshold": 50,
|
||||
"criticalThreshold": 20
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
Response (score 42.5 is below the warning threshold of 50):
|
||||
```json
|
||||
{
|
||||
"states": [
|
||||
{
|
||||
"status": 3,
|
||||
"message": "Score: 42.5 - test",
|
||||
"code": "dummy_score_check"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Status codes: `1` = OK, `3` = Warning, `4` = Critical.
|
||||
|
||||
### Extract metrics
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/report \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"data": "{\"message\":\"test\",\"score\":73.2,\"collected_at\":\"2026-01-15T10:30:00Z\"}"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Deploying to happyDomain
|
||||
|
||||
### As an external checker (recommended)
|
||||
|
||||
1. Deploy your checker as a standalone service (Docker, systemd, etc.).
|
||||
2. In happyDomain, set the checker's `endpoint` admin option to its URL (e.g., `http://checker-dummy:8080`).
|
||||
3. happyDomain will call `/collect`, `/evaluate`, and `/report` automatically.
|
||||
|
||||
### As an in-process plugin
|
||||
|
||||
1. Build the plugin:
|
||||
```bash
|
||||
go build -buildmode=plugin -o checker-dummy.so ./plugin
|
||||
```
|
||||
2. Copy `checker-dummy.so` into one of the directories listed in happyDomain's `PluginsDirectories` configuration.
|
||||
3. Restart happyDomain. At startup it scans those directories, opens each `.so`, looks up the `NewCheckerPlugin` symbol, and registers the returned definition and provider.
|
||||
|
||||
---
|
||||
|
||||
## License & happyDomain compatibility
|
||||
|
||||
This template is released under the **MIT License**, so you're free to use it as a starting point for any checker, including proprietary ones.
|
||||
|
||||
**Important caveat about the deployment mode you choose:**
|
||||
|
||||
- **Standalone HTTP checker:** your checker is a separate process communicating with happyDomain over the network. It is *not* a derivative work of happyDomain and you can license it however you want (proprietary, MIT, GPL, anything).
|
||||
- **In-process plugin (`.so`):** your checker is loaded into the happyDomain process via `plugin.Open` and links against happyDomain's types at runtime. The FSF considers this a derivative work, so your plugin must be licensed under a license compatible with happyDomain's **AGPL-3.0** (typically AGPL-3.0 itself, or GPL-3.0).
|
||||
- **Built-in checker** (imported directly into the happyDomain source tree): same constraint: AGPL-3.0.
|
||||
|
||||
If in doubt, pick the standalone mode: it gives you the most licensing freedom and avoids Go's plugin toolchain headaches.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Going Further
|
||||
|
||||
Now that you understand the structure, here are ideas for your own checker:
|
||||
|
||||
- **HTTP checker**: send an HTTP request to a domain's web server and check the status code, response time, or TLS certificate expiry.
|
||||
- **DNS checker**: query specific DNS record types and verify the response matches expectations.
|
||||
- **SMTP checker**: connect to a mail server and verify it responds correctly to EHLO.
|
||||
- **Whois checker**: check domain expiry date and alert before it lapses.
|
||||
|
||||
For a real-world example, look at [checker-ping](https://git.happydns.org/happyDomain/checker-ping), which implements ICMP ping monitoring with multiple targets, packet loss detection, and RTT metrics.
|
||||
|
||||
### Tips
|
||||
|
||||
- Keep `Collect` focused on data gathering. Put all threshold logic in `Evaluate`.
|
||||
- Design your data struct to hold everything rules need; evaluation cannot re-collect.
|
||||
- Use `sdk.GetFloatOption` / `sdk.GetIntOption` / `sdk.GetBoolOption` instead of raw type assertions. They handle the JSON/native type mismatch transparently.
|
||||
- Always honour the `context.Context`: set timeouts and check for cancellation.
|
||||
- Return partial results from `Collect` when possible (only return an error if the entire collection failed).
|
||||
47
checker/collect.go
Normal file
47
checker/collect.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand/v2"
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
sdk "git.happydns.org/happyDomain/sdk/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 happydns.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
116
checker/definition.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// 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() *happydns.CheckerDefinition {
|
||||
return &happydns.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: happydns.CheckerAvailability{
|
||||
ApplyToDomain: true,
|
||||
},
|
||||
|
||||
// ObservationKeys lists the keys this checker produces. This ties
|
||||
// the definition to the provider(s) that generate the data.
|
||||
ObservationKeys: []happydns.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: happydns.CheckerOptionsDocumentation{
|
||||
UserOpts: []happydns.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: []happydns.CheckerOptionDocumentation{
|
||||
{
|
||||
Id: "domain_name",
|
||||
Label: "Domain name",
|
||||
AutoFill: happydns.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: []happydns.CheckRule{
|
||||
Rule(),
|
||||
},
|
||||
|
||||
// Interval specifies how often the check should run.
|
||||
Interval: &happydns.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
61
checker/provider.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// 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() happydns.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() happydns.ObservationKey {
|
||||
return ObservationKeyDummy
|
||||
}
|
||||
|
||||
// Definition implements happydns.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() *happydns.CheckerDefinition {
|
||||
return Definition()
|
||||
}
|
||||
|
||||
// ExtractMetrics implements happydns.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) ([]happydns.CheckMetric, error) {
|
||||
var data DummyData
|
||||
if err := json.Unmarshal(raw, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []happydns.CheckMetric{
|
||||
{
|
||||
Name: "dummy_score",
|
||||
Value: data.Score,
|
||||
Unit: "points",
|
||||
Timestamp: collectedAt,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
97
checker/rule.go
Normal file
97
checker/rule.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
sdk "git.happydns.org/happyDomain/sdk/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() happydns.CheckRule {
|
||||
return &dummyRule{}
|
||||
}
|
||||
|
||||
// dummyRule implements the happydns.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 happydns.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 happydns.ObservationGetter, opts happydns.CheckerOptions) happydns.CheckState {
|
||||
// Retrieve the observation data by key.
|
||||
var data DummyData
|
||||
if err := obs.Get(ctx, ObservationKeyDummy, &data); err != nil {
|
||||
return happydns.CheckState{
|
||||
Status: happydns.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 happydns.Status
|
||||
switch {
|
||||
case data.Score < criticalThreshold:
|
||||
status = happydns.StatusCrit
|
||||
case data.Score < warningThreshold:
|
||||
status = happydns.StatusWarn
|
||||
default:
|
||||
status = happydns.StatusOK
|
||||
}
|
||||
|
||||
return happydns.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
33
checker/types.go
Normal 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"`
|
||||
}
|
||||
44
go.mod
Normal file
44
go.mod
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
module git.happydns.org/happyDomain/checker-dummy
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require git.happydns.org/happyDomain v0.6.0
|
||||
|
||||
require (
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.12.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
golang.org/x/arch v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
replace git.happydns.org/happyDomain => ../happydomain3
|
||||
98
go.sum
Normal file
98
go.sum
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
30
main.go
Normal file
30
main.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
dummy "git.happydns.org/happyDomain/checker-dummy/checker"
|
||||
sdk "git.happydns.org/happyDomain/sdk/checker"
|
||||
)
|
||||
|
||||
// Version is the standalone binary's version. It defaults to "custom-build"
|
||||
// and is meant to be overridden by the CI at link time:
|
||||
//
|
||||
// go build -ldflags "-X main.Version=1.2.3" .
|
||||
var Version = "custom-build"
|
||||
|
||||
var listenAddr = flag.String("listen", ":8080", "HTTP listen address")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
// Propagate the binary version to the checker package so it shows up in
|
||||
// CheckerDefinition.Version.
|
||||
dummy.Version = Version
|
||||
|
||||
server := sdk.NewServer(dummy.Provider())
|
||||
if err := server.ListenAndServe(*listenAddr); err != nil {
|
||||
log.Fatalf("server error: %v", err)
|
||||
}
|
||||
}
|
||||
26
plugin/plugin.go
Normal file
26
plugin/plugin.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Command plugin is the happyDomain plugin entrypoint for the dummy checker.
|
||||
//
|
||||
// It is built as a Go plugin (`go build -buildmode=plugin`) and loaded at
|
||||
// runtime by happyDomain.
|
||||
package main
|
||||
|
||||
import (
|
||||
dummy "git.happydns.org/happyDomain/checker-dummy/checker"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// Version is the plugin's version. It defaults to "custom-build" and is
|
||||
// meant to be overridden by the CI at link time:
|
||||
//
|
||||
// go build -buildmode=plugin -ldflags "-X main.Version=1.2.3" -o checker-dummy.so ./plugin
|
||||
var Version = "custom-build"
|
||||
|
||||
// NewCheckerPlugin is the symbol resolved by happyDomain when loading the
|
||||
// .so file. It returns the checker definition and the observation provider
|
||||
// that the host will register in its global registries.
|
||||
func NewCheckerPlugin() (*happydns.CheckerDefinition, happydns.ObservationProvider, error) {
|
||||
// Propagate the plugin's version to the checker package so it shows up
|
||||
// in CheckerDefinition.Version.
|
||||
dummy.Version = Version
|
||||
return dummy.Definition(), dummy.Provider(), nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue