Guide for building, testing, and contributing to Registry.
- Go version declared in
go.mod - Node.js 18 or later
- Python 3.10 or later
- Git
- Make (optional, for convenience)
git clone https://github.com/burgrp/reg.git
cd reg# Build binary
go build -o reg
# Build with version information
VERSION=1.0.0
go build -ldflags="-X 'github.com/burgrp/reg/cmd.Version=$VERSION'" -o reg
# Verify build
./reg version# Run all tests
go test ./...
# Run tests with verbose output
go test -v ./...
# Run tests without cache
go test -count=1 ./...
# Run specific test
go test -v ./pkg/client/rest -run TestClient_Consume
# Run with race detector
go test -race ./...
# Run with coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.outRun the client library suites:
npm --prefix clients/js test
npm --prefix clients/node-red test
python -m pip install -e './clients/python[dev]'
python -m unittest discover -s clients/python/test -v
ruff check clients/python
ruff format --check clients/python
mypy clients/python/src# Start on default port (8080)
./reg serve
# Start on custom port
./reg serve --rest :9000
# With custom logging
LOG_LEVEL=debug ./reg serveOd v1.0.17 řídí LOG_LEVEL diagnostické logování všech příkazů, včetně
serveru registry, jeho REST rozhraní, metrik a MCP serveru. Logy se zapisují
na standardní chybový výstup (stderr). Výsledky příkazů jako get, list
nebo version ani JSON-RPC komunikace MCP na stdout se nefiltrují.
Chybová hlášení CLI zůstávají viditelná bez ohledu na úroveň logování.
Výchozí úroveň při nenastavené nebo prázdné proměnné je INFO.
Přijímají se debug, info, warn a error bez ohledu na velikost písmen,
také s číselným posunem podle Go log/slog, například INFO+2.
Neplatná hodnota ukončí příkaz chybou ještě před jeho spuštěním;
nápověda zůstává dostupná.
LOG_LEVEL=info ./reg serve --metrics :9090
LOG_LEVEL=debug ./reg serve
LOG_LEVEL=error REGISTRY=http://localhost:8080 ./reg listNastavení popisuje hlavní nápověda (reg, reg --help) i reg serve --help.
Samostatný přepínač pro úroveň logování není zavedený.
Starší verze LOG_LEVEL nečetly a serve měl pevně nastavenou úroveň DEBUG.
Export metrik se zapíná samostatným HTTP rozhraním:
./reg serve --rest :8080 --metrics :9090Exporter poskytuje na / i /metrics číselné registry jako metriky typu
gauge; booleovské hodnoty převádí na 0 a 1. Ostatní typy neexportuje.
Nemění názvy ani hodnoty registrů v REST rozhraní.
V názvu metriky nahrazuje tečky dvojtečkami. Platné názvy zachovává,
před počáteční číslici přidává _ a znaky mimo ASCII množinu
[a-zA-Z0-9_:] nahrazuje _. Prázdný název převádí na _.
Například registr nepojmenovaného Zigbee zařízení
0x00158d000483b274.temperature exportuje jako
_0x00158d000483b274:temperature. Takové zařízení proto nezneplatní
celý scrape kvůli názvu začínajícímu číslicí.
Štítky n1, n2 atd. obsahují původní části názvu před touto normalizací.
Registry jako room-1.temperature a room_1.temperature tak mají
stejný název metriky, ale odlišné štítky a samostatné hodnoty.
Deklarace TYPE se pro společný název zapisuje jen jednou.
Existující platné názvy metrik a jejich štítky zůstávají beze změny.
Regresní testy exportu:
go test -race ./internal/metricsreg/
├── cmd/ # CLI commands
│ ├── root.go # Root command
│ ├── serve.go # Server command
│ ├── provide.go # Provider CLI command
│ ├── get.go # Consumer read command
│ ├── list.go # List registers command
│ ├── browse.go # Interactive TUI
│ └── version.go # Version command
├── internal/ # Private application code
│ ├── registry/ # Core business logic
│ │ ├── core.go # Registry implementation
│ │ └── listener.go # Notification system
│ └── rest/ # REST protocol layer
│ ├── server.go # HTTP server
│ ├── consumer.go # Consumer endpoints
│ ├── provider.go # Provider endpoints
│ └── duration.go # Duration JSON handling
├── pkg/ # Public library code
│ ├── client/ # Client interface
│ │ ├── client.go # Protocol-agnostic interface
│ │ ├── factory/ # Client factory
│ │ └── rest/ # REST client implementation
│ │ ├── client.go # Client struct
│ │ ├── consumer.go # Consumer implementation
│ │ └── provider.go # Provider implementation
│ ├── wire/ # Wire protocol clients
│ │ └── rest/ # Low-level REST clients
│ └── browser/ # TUI browser
├── docs/ # Documentation
├── clients/ # JavaScript, Node-RED, and Python clients
├── main.go # Entry point
├── go.mod # Go modules
└── build.sh # Release build script
- Create a feature branch:
git checkout -b feature/my-feature-
Make your changes
-
Run tests:
go test ./...- Format code:
go fmt ./...- Run linters:
go vet ./...
golangci-lint run # If installed- Commit changes:
git add .
git commit -m "Add feature: description"Terminal 1 (Server):
./reg serveTerminal 2 (Provider):
export REGISTRY=http://localhost:8080
./reg provide temperature 22.5 '{"unit":"celsius"}' --ttl 10sTerminal 3 (Consumer):
export REGISTRY=http://localhost:8080
./reg get temperature# Start server in background
./reg serve &
SERVER_PID=$!
# Run tests against server
export REGISTRY=http://localhost:8080
# Test provider
./reg provide test 123 &
PROVIDER_PID=$!
sleep 1
# Test consumer
RESULT=$(./reg get test)
echo "$RESULT"
# Cleanup
kill $PROVIDER_PID $SERVER_PIDpackage rest
import (
"testing"
)
func TestMyFunction(t *testing.T) {
// Arrange
input := "test"
// Act
result := myFunction(input)
// Assert
if result != expected {
t.Errorf("got %v, want %v", result, expected)
}
}func TestServerIntegration(t *testing.T) {
// Create test server
server := httptest.NewServer(handler)
defer server.Close()
// Test client
client := rest.NewClient(server.URL)
// Run test
ctx := context.Background()
values, _, err := client.Consume(ctx, "test")
if err != nil {
t.Fatal(err)
}
// Verify
select {
case v := <-values:
if v.Value != expected {
t.Errorf("got %v, want %v", v.Value, expected)
}
case <-time.After(1 * time.Second):
t.Fatal("timeout")
}
}func TestDuration_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
input string
want time.Duration
wantErr bool
}{
{"seconds", `"5s"`, 5 * time.Second, false},
{"minutes", `"10m"`, 10 * time.Minute, false},
{"invalid", `"invalid"`, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var d Duration
err := d.UnmarshalJSON([]byte(tt.input))
if (err != nil) != tt.wantErr {
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
return
}
if time.Duration(d) != tt.want {
t.Errorf("UnmarshalJSON() = %v, want %v", d, tt.want)
}
})
}
}import "log/slog"
// In main.go or test
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
slog.SetDefault(logger)# Install delve
go install github.com/go-delve/delve/cmd/dlv@latest
# Debug test
dlv test ./pkg/client/rest -- -test.run TestClient_Consume
# Debug application
dlv debug . -- serve --rest :8080
# Common commands
(dlv) break main.main
(dlv) continue
(dlv) print variable
(dlv) next
(dlv) step
(dlv) quitclient := rest.NewClientWithHTTPClient("http://localhost:8080", &http.Client{
Transport: &http.Transport{
// Enable tracing
DisableKeepAlives: true,
},
})Or use environment variable:
export GODEBUG=http2debug=1
./reg servefunc BenchmarkRegistry_SetRegister(b *testing.B) {
r := registry.New()
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
r.SetRegister(ctx, "test", i, nil, 10*time.Second)
}
}Run benchmarks:
# Run all benchmarks
go test -bench=. ./...
# Run specific benchmark
go test -bench=BenchmarkRegistry_SetRegister ./internal/registry
# With memory profiling
go test -bench=. -benchmem ./...
# Generate CPU profile
go test -bench=. -cpuprofile=cpu.prof ./...
go tool pprof cpu.prof# Install hey
go install github.com/rakyll/hey@latest
# Load test provider endpoint
hey -n 10000 -c 100 -m PUT \
-H "Content-Type: application/json" \
-d '{"registers":{"temp":{"value":22.5,"ttl":"10s"}}}' \
http://localhost:8080/provider
# Load test consumer endpoint
hey -n 10000 -c 100 \
http://localhost:8080/consumer?name=tempTests run automatically on push via .github/workflows/test.yml:
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- run: go test ./...Create .git/hooks/pre-commit:
#!/bin/bash
echo "Running tests..."
go test ./...
if [ $? -ne 0 ]; then
echo "Tests failed"
exit 1
fi
echo "Running go fmt..."
go fmt ./...
echo "Running go vet..."
go vet ./...
if [ $? -ne 0 ]; then
echo "go vet failed"
exit 1
fi
exit 0Make it executable:
chmod +x .git/hooks/pre-commit# Tag release
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0Build script creates binaries for multiple platforms:
# Build for all platforms
GITHUB_REF=refs/tags/v1.0.0 ./build.sh
# Binaries created in bin/
ls bin/
# reg-linux-amd64
# reg-linux-arm64
# reg-darwin-amd64
# reg-darwin-arm64
# reg-windows-amd64.exe# Linux AMD64
GOOS=linux GOARCH=amd64 go build \
-ldflags="-X 'github.com/burgrp/reg/cmd.Version=1.0.0'" \
-o reg-linux-amd64
# macOS ARM64
GOOS=darwin GOARCH=arm64 go build \
-ldflags="-X 'github.com/burgrp/reg/cmd.Version=1.0.0'" \
-o reg-darwin-arm64
# Windows AMD64
GOOS=windows GOARCH=amd64 go build \
-ldflags="-X 'github.com/burgrp/reg/cmd.Version=1.0.0'" \
-o reg-windows-amd64.exe- Create command file in
cmd/:
// cmd/mycommand.go
package cmd
import "github.com/spf13/cobra"
func newMyCommandCmd() *cobra.Command {
return &cobra.Command{
Use: "mycommand",
Short: "Description",
RunE: func(cmd *cobra.Command, args []string) error {
// Implementation
return nil
},
}
}- Register in root command:
// cmd/root.go
func newRootCmd() *cobra.Command {
root := &cobra.Command{...}
root.AddCommand(
newServeCmd(),
newMyCommandCmd(), // Add here
)
return root
}- Add handler to
internal/rest/:
// internal/rest/myendpoint.go
func handleMyEndpoint(reg *registry.Registry) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Implementation
}
}- Register in server:
// internal/rest/server.go
func RunServer(ctx context.Context, addr string, reg *registry.Registry) error {
mux := http.NewServeMux()
mux.HandleFunc("/myendpoint", handleMyEndpoint(reg))
// ...
}- Add to interface:
// pkg/client/client.go
type Client interface {
Consume(...) (...)
Provide(...) (...)
MyNewMethod(...) (...) // Add here
}- Implement in REST client:
// pkg/client/rest/myclient.go
func (c *Client) MyNewMethod(...) (...) {
// Implementation
}# Clear test cache
go clean -testcache
# Run with verbose output
go test -v ./...
# Run specific failing test
go test -v ./pkg -run TestNameReorganize code:
- Move shared types to separate package
- Use interfaces to break dependencies
- Ensure
internal/doesn't importpkg/
# Run with race detector
go test -race ./...
# Fix by:
# - Adding mutexes
# - Using channels
# - Avoiding shared state# Format all code
go fmt ./...
# Check formatting
gofmt -l .- Packages: lowercase, single word
- Exported: PascalCase
- Unexported: camelCase
- Interfaces: noun or -er suffix
- Constants: PascalCase or SCREAMING_SNAKE_CASE
// Package rest implements the REST protocol layer.
package rest
// Client implements the client.Client interface using REST.
type Client struct {
// consumerClient handles consumer operations.
consumerClient *wire.ConsumerClient
}
// NewClient creates a new REST client with the given base URL.
func NewClient(baseURL string) *Client {
// Implementation
}