Tidying up

This commit is contained in:
2025-10-16 22:10:47 -07:00
parent d28bb92486
commit ba3fad9431
13 changed files with 225 additions and 296 deletions
+4 -6
View File
@@ -16,9 +16,8 @@ RUN go mod download
# Copy source code # Copy source code
COPY . . COPY . .
# Build binaries # Build binary
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server ./cmd/server RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server ./cmd/server
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o restserver ./cmd/restserver
# Runtime stage # Runtime stage
FROM alpine:latest FROM alpine:latest
@@ -33,9 +32,8 @@ RUN addgroup -g 1000 notifier && \
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
# Copy binaries from builder # Copy binary from builder
COPY --from=builder /build/server /app/ COPY --from=builder /build/server /app/
COPY --from=builder /build/restserver /app/
# Copy default config (can be overridden with volume mount) # Copy default config (can be overridden with volume mount)
COPY config.yaml /app/config.yaml COPY config.yaml /app/config.yaml
@@ -50,6 +48,6 @@ USER notifier
# Expose ports # Expose ports
EXPOSE 8080 50051 9090 8081 EXPOSE 8080 50051 9090 8081
# Default to running the combined server (handles both REST and gRPC) # Run server (defaults to both REST and gRPC)
# Can be overridden with docker run command # Override mode with environment variable: -e SERVER_MODE=rest or -e SERVER_MODE=grpc
CMD ["/app/server"] CMD ["/app/server"]
+93 -26
View File
@@ -1,10 +1,11 @@
.PHONY: proto proto-gen proto-clean deps build run-grpc run-rest run-both test lint docker-build docker-run clean .PHONY: proto proto-gen proto-clean deps build run run-grpc run-rest test lint fmt vet check docker-build docker-run clean help
# Variables # Variables
PROTO_DIR=api/grpc PROTO_DIR=api/grpc
PROTO_FILE=$(PROTO_DIR)/notifier.proto PROTO_FILE=$(PROTO_DIR)/notifier.proto
PROTO_OUT=$(PROTO_DIR)/pb PROTO_OUT=$(PROTO_DIR)/pb
GO_MODULE=$(shell head -n 1 go.mod | awk '{print $$2}') GO_MODULE=$(shell head -n 1 go.mod | awk '{print $$2}')
GO_FILES=$(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -path "./api/grpc/pb/*")
# Generate protobuf code # Generate protobuf code
proto-gen: proto-gen:
@@ -35,33 +36,78 @@ proto-deps:
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
@echo "Protoc plugins installed" @echo "Protoc plugins installed"
# Build binaries # Build binary
build: build:
@echo "Building binaries..." @echo "Building binary..."
@mkdir -p bin @mkdir -p bin
go build -o bin/grpcserver ./cmd/grpcserver go build -o bin/server ./cmd/server
go build -o bin/restserver ./cmd/restserver @echo "Binary built successfully"
@echo "Binaries built successfully"
# Run gRPC server # Run server (default: both REST and gRPC)
run-grpc: run:
@echo "Running gRPC server..." @echo "Running server (both REST and gRPC)..."
go run ./cmd/grpcserver/main.go go run ./cmd/server/main.go
# Run REST server # Run in REST-only mode
run-rest: run-rest:
@echo "Running REST server..." @echo "Running server in REST-only mode..."
go run ./cmd/restserver/main.go SERVER_MODE=rest go run ./cmd/server/main.go
# Run in gRPC-only mode
run-grpc:
@echo "Running server in gRPC-only mode..."
SERVER_MODE=grpc go run ./cmd/server/main.go
# Run tests # Run tests
test: test:
@echo "Running tests..." @echo "Running tests..."
go test -v -race -cover ./... go test -v -race -cover ./...
# Run linter # Run tests with coverage report
test-coverage:
@echo "Running tests with coverage..."
go test -race -coverprofile=coverage.out -covermode=atomic ./...
go tool cover -html=coverage.out -o coverage.html
@echo "Coverage report generated: coverage.html"
# Format code
fmt:
@echo "Formatting code..."
gofmt -s -w $(GO_FILES)
@echo "Code formatted"
# Check formatting
fmt-check:
@echo "Checking code formatting..."
@if [ -n "$$(gofmt -l $(GO_FILES))" ]; then \
echo "The following files need formatting:"; \
gofmt -l $(GO_FILES); \
exit 1; \
fi
@echo "All files are properly formatted"
# Run go vet
vet:
@echo "Running go vet..."
go vet ./...
@echo "go vet passed"
# Run static analysis
check: fmt-check vet
@echo "Running static checks..."
go mod verify
@echo "All checks passed"
# Run linter (requires golangci-lint)
lint: lint:
@echo "Running linter..." @echo "Running linter..."
@which golangci-lint > /dev/null || (echo "golangci-lint not installed. Run: brew install golangci-lint" && exit 1)
golangci-lint run ./... golangci-lint run ./...
@echo "Linting passed"
# Run all quality checks
qa: fmt vet lint test
@echo "All quality checks passed!"
# Build Docker image # Build Docker image
docker-build: docker-build:
@@ -84,15 +130,36 @@ clean:
# Help # Help
help: help:
@echo "Available targets:" @echo "Available targets:"
@echo " proto-gen - Generate protobuf code" @echo ""
@echo " proto-clean - Clean generated protobuf code" @echo "Build:"
@echo " proto-deps - Install protoc plugins" @echo " build - Build server binary"
@echo " deps - Install Go dependencies" @echo " clean - Clean build artifacts"
@echo " build - Build binaries" @echo ""
@echo " run-grpc - Run gRPC server" @echo "Run:"
@echo " run-rest - Run REST server" @echo " run - Run server (both REST and gRPC)"
@echo " test - Run tests" @echo " run-rest - Run server in REST-only mode"
@echo " lint - Run linter" @echo " run-grpc - Run server in gRPC-only mode"
@echo " docker-build - Build Docker image" @echo ""
@echo " docker-run - Run Docker container" @echo "Test:"
@echo " clean - Clean build artifacts" @echo " test - Run tests with race detector"
@echo " test-coverage - Run tests with coverage report"
@echo ""
@echo "Code Quality:"
@echo " fmt - Format code with gofmt"
@echo " fmt-check - Check if code is formatted"
@echo " vet - Run go vet"
@echo " lint - Run golangci-lint (requires installation)"
@echo " check - Run fmt-check + vet + mod verify"
@echo " qa - Run all quality checks (fmt + vet + lint + test)"
@echo ""
@echo "Protobuf:"
@echo " proto-gen - Generate protobuf code"
@echo " proto-clean - Clean generated protobuf code"
@echo " proto-deps - Install protoc plugins"
@echo ""
@echo "Dependencies:"
@echo " deps - Install Go dependencies"
@echo ""
@echo "Docker:"
@echo " docker-build - Build Docker image"
@echo " docker-run - Run Docker container"
+34 -10
View File
@@ -45,7 +45,7 @@ cd notifier
go mod tidy go mod tidy
# Build and run # Build and run
go build -o bin/server ./cmd/server make build
./bin/server ./bin/server
``` ```
@@ -54,6 +54,13 @@ Server starts with:
- **gRPC API** on `localhost:50051` - **gRPC API** on `localhost:50051`
- **Stdout notifier** enabled by default - **Stdout notifier** enabled by default
**Run in different modes:**
```bash
./bin/server # Both REST and gRPC (default)
SERVER_MODE=rest ./bin/server # REST only
SERVER_MODE=grpc ./bin/server # gRPC only
```
### 2. Send Your First Notification ### 2. Send Your First Notification
```bash ```bash
@@ -299,6 +306,18 @@ docker run -d \
notifier:latest notifier:latest
``` ```
**Run in different modes:**
```bash
# Both REST and gRPC (default)
docker run -d -p 8080:8080 -p 50051:50051 notifier:latest
# REST only
docker run -d -p 8080:8080 -e SERVER_MODE=rest notifier:latest
# gRPC only
docker run -d -p 50051:50051 -e SERVER_MODE=grpc notifier:latest
```
**Docker Compose:** **Docker Compose:**
```bash ```bash
docker-compose up -d docker-compose up -d
@@ -391,9 +410,7 @@ notifier/
│ ├── router.go # Route configuration │ ├── router.go # Route configuration
│ └── types.go # Request/response types │ └── types.go # Request/response types
├── cmd/ ├── cmd/
── server/main.go # Combined server (recommended) ── server/main.go # Unified server (configurable mode)
│ ├── grpcserver/main.go # gRPC only
│ └── restserver/main.go # REST only
├── internal/ ├── internal/
│ ├── config/ │ ├── config/
│ │ └── config.go # Configuration management │ │ └── config.go # Configuration management
@@ -434,14 +451,21 @@ notifier/
### Build Commands ### Build Commands
```bash ```bash
make build # Build binaries make build # Build server binary
make run-rest # Run REST server make run # Run server (both REST and gRPC)
make run-grpc # Run gRPC server (when implemented) make run-rest # Run server in REST-only mode
make test # Run tests make run-grpc # Run server in gRPC-only mode
make lint # Run linter make test # Run tests with race detector
make test-coverage # Generate HTML coverage report
make fmt # Format code with gofmt
make vet # Run go vet
make lint # Run golangci-lint
make check # Run fmt-check + vet + mod verify
make qa # Run all quality checks
make proto-gen # Generate protobuf code make proto-gen # Generate protobuf code
make docker-build # Build Docker image make docker-build # Build Docker image
make clean # Clean artifacts make clean # Clean build artifacts
make help # Show all available targets
``` ```
### Adding a New Notifier ### Adding a New Notifier
View File
-174
View File
@@ -1,174 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/igodwin/notifier/api/rest"
"github.com/igodwin/notifier/internal/config"
"github.com/igodwin/notifier/internal/domain"
"github.com/igodwin/notifier/internal/notifier"
"github.com/igodwin/notifier/internal/queue"
"github.com/igodwin/notifier/internal/service"
)
func main() {
// Load configuration
cfg, err := config.Load("")
if err != nil {
log.Printf("Warning: failed to load config, using defaults: %v", err)
cfg = getDefaultConfig()
}
log.Printf("Starting Notifier REST Server on port %d", cfg.Server.RESTPort)
// Create context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Initialize queue
var q domain.Queue
if cfg.Queue.Type == "local" {
q, err = queue.NewLocalQueue(cfg.Queue.Local)
if err != nil {
log.Fatalf("Failed to create queue: %v", err)
}
log.Println("Using local queue")
} else {
log.Fatalf("Queue type %s not implemented yet", cfg.Queue.Type)
}
// Initialize notifier factory
factory := notifier.NewFactory()
// Register notifiers based on configuration
if cfg.Notifiers.Stdout {
stdoutNotifier := notifier.NewStdoutNotifier()
if err := factory.RegisterNotifier(domain.TypeStdout, stdoutNotifier); err != nil {
log.Fatalf("Failed to register stdout notifier: %v", err)
}
log.Println("Registered stdout notifier")
}
if cfg.Notifiers.SMTP != nil {
smtpNotifier, err := notifier.NewSMTPNotifier(cfg.Notifiers.SMTP)
if err != nil {
log.Printf("Warning: failed to create SMTP notifier: %v", err)
} else {
if err := factory.RegisterNotifier(domain.TypeEmail, smtpNotifier); err != nil {
log.Fatalf("Failed to register SMTP notifier: %v", err)
}
log.Println("Registered SMTP notifier")
}
}
if cfg.Notifiers.Slack != nil {
slackNotifier, err := notifier.NewSlackNotifier(cfg.Notifiers.Slack)
if err != nil {
log.Printf("Warning: failed to create Slack notifier: %v", err)
} else {
if err := factory.RegisterNotifier(domain.TypeSlack, slackNotifier); err != nil {
log.Fatalf("Failed to register Slack notifier: %v", err)
}
log.Println("Registered Slack notifier")
}
}
if cfg.Notifiers.Ntfy != nil {
ntfyNotifier, err := notifier.NewNtfyNotifier(cfg.Notifiers.Ntfy)
if err != nil {
log.Printf("Warning: failed to create Ntfy notifier: %v", err)
} else {
if err := factory.RegisterNotifier(domain.TypeNtfy, ntfyNotifier); err != nil {
log.Fatalf("Failed to register Ntfy notifier: %v", err)
}
log.Println("Registered Ntfy notifier")
}
}
// Check if any notifiers are registered
if len(factory.SupportedTypes()) == 0 {
log.Fatal("No notifiers configured. Please enable at least one notifier in config.yaml")
}
log.Printf("Supported notification types: %v", factory.SupportedTypes())
// Create notification service
svc := service.NewNotificationService(factory, q, cfg.Queue.WorkerCount)
// Start workers
if err := svc.Start(ctx); err != nil {
log.Fatalf("Failed to start service: %v", err)
}
log.Printf("Started %d worker(s)", cfg.Queue.WorkerCount)
// Create REST router
router := rest.NewRouter(svc)
// Create HTTP server
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.RESTPort)
server := &http.Server{
Addr: addr,
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Start server in goroutine
go func() {
log.Printf("REST server listening on %s", addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to start server: %v", err)
}
}()
// Wait for interrupt signal
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
log.Println("Shutting down server...")
// Graceful shutdown
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer shutdownCancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Printf("Error during server shutdown: %v", err)
}
// Stop service
if err := svc.Stop(); err != nil {
log.Printf("Error stopping service: %v", err)
}
log.Println("Server stopped")
}
// getDefaultConfig returns a minimal default configuration
func getDefaultConfig() *config.Config {
return &config.Config{
Server: config.ServerConfig{
RESTPort: 8080,
Host: "0.0.0.0",
},
Queue: domain.QueueConfig{
Type: "local",
WorkerCount: 5,
RetryAttempts: 3,
Local: &domain.LocalQueueConfig{
BufferSize: 1000,
},
},
Notifiers: config.NotifiersConfig{
Stdout: true,
},
}
}
+9 -8
View File
@@ -1,18 +1,19 @@
module github.com/igodwin/notifier module github.com/igodwin/notifier
go 1.23.2 go 1.24.0
toolchain go1.24.6
require ( require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/spf13/viper v1.19.0 github.com/spf13/viper v1.19.0
google.golang.org/grpc v1.62.1 google.golang.org/grpc v1.76.0
google.golang.org/protobuf v1.33.0 google.golang.org/protobuf v1.36.10
) )
require ( require (
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -27,10 +28,10 @@ require (
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.23.0 // indirect golang.org/x/net v0.46.0 // indirect
golang.org/x/sys v0.24.0 // indirect golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.17.0 // indirect golang.org/x/text v0.30.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+34 -21
View File
@@ -6,12 +6,14 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
@@ -59,27 +61,38 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f h1:1FTH6cpXFsENbPR5Bu8NQddPSaUUE6NA2XdZdDSAJK4=
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+10 -10
View File
@@ -11,12 +11,12 @@ import (
// Config represents the application configuration // Config represents the application configuration
type Config struct { type Config struct {
Server ServerConfig `mapstructure:"server"` Server ServerConfig `mapstructure:"server"`
Queue domain.QueueConfig `mapstructure:"queue"` Queue domain.QueueConfig `mapstructure:"queue"`
Notifiers NotifiersConfig `mapstructure:"notifiers"` Notifiers NotifiersConfig `mapstructure:"notifiers"`
Logging LoggingConfig `mapstructure:"logging"` Logging LoggingConfig `mapstructure:"logging"`
Metrics MetricsConfig `mapstructure:"metrics"` Metrics MetricsConfig `mapstructure:"metrics"`
HealthCheck HealthCheckConfig `mapstructure:"health_check"` HealthCheck HealthCheckConfig `mapstructure:"health_check"`
} }
// ServerConfig contains server configuration // ServerConfig contains server configuration
@@ -44,10 +44,10 @@ type LoggingConfig struct {
// MetricsConfig contains metrics/observability configuration // MetricsConfig contains metrics/observability configuration
type MetricsConfig struct { type MetricsConfig struct {
Enabled bool `mapstructure:"enabled"` Enabled bool `mapstructure:"enabled"`
Port int `mapstructure:"port"` Port int `mapstructure:"port"`
Path string `mapstructure:"path"` Path string `mapstructure:"path"`
PrometheusEnabled bool `mapstructure:"prometheus_enabled"` PrometheusEnabled bool `mapstructure:"prometheus_enabled"`
} }
// HealthCheckConfig contains health check configuration // HealthCheckConfig contains health check configuration
+8 -8
View File
@@ -104,12 +104,12 @@ type NotificationResult struct {
// NotificationFilter is used for querying notifications // NotificationFilter is used for querying notifications
type NotificationFilter struct { type NotificationFilter struct {
IDs []string `json:"ids,omitempty"` IDs []string `json:"ids,omitempty"`
Types []NotificationType `json:"types,omitempty"` Types []NotificationType `json:"types,omitempty"`
Statuses []NotificationStatus `json:"statuses,omitempty"` Statuses []NotificationStatus `json:"statuses,omitempty"`
Recipients []string `json:"recipients,omitempty"` Recipients []string `json:"recipients,omitempty"`
CreatedAfter *time.Time `json:"created_after,omitempty"` CreatedAfter *time.Time `json:"created_after,omitempty"`
CreatedBefore *time.Time `json:"created_before,omitempty"` CreatedBefore *time.Time `json:"created_before,omitempty"`
Limit int `json:"limit,omitempty"` Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"` Offset int `json:"offset,omitempty"`
} }
+7 -7
View File
@@ -57,11 +57,11 @@ type NotificationService interface {
// NotificationStats contains statistics about notification processing // NotificationStats contains statistics about notification processing
type NotificationStats struct { type NotificationStats struct {
TotalSent int64 `json:"total_sent"` TotalSent int64 `json:"total_sent"`
TotalFailed int64 `json:"total_failed"` TotalFailed int64 `json:"total_failed"`
TotalPending int64 `json:"total_pending"` TotalPending int64 `json:"total_pending"`
TotalQueued int64 `json:"total_queued"` TotalQueued int64 `json:"total_queued"`
ByType map[string]int64 `json:"by_type"` ByType map[string]int64 `json:"by_type"`
ByStatus map[string]int64 `json:"by_status"` ByStatus map[string]int64 `json:"by_status"`
AverageLatency float64 `json:"average_latency_ms"` AverageLatency float64 `json:"average_latency_ms"`
} }
+10 -10
View File
@@ -43,17 +43,17 @@ type NtfyNotifier struct {
// ntfyRequest represents the ntfy API request format // ntfyRequest represents the ntfy API request format
type ntfyRequest struct { type ntfyRequest struct {
Topic string `json:"topic"` Topic string `json:"topic"`
Message string `json:"message"` Message string `json:"message"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Priority int `json:"priority,omitempty"` Priority int `json:"priority,omitempty"`
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
Click string `json:"click,omitempty"` Click string `json:"click,omitempty"`
Attach string `json:"attach,omitempty"` Attach string `json:"attach,omitempty"`
Actions []ntfyAction `json:"actions,omitempty"` Actions []ntfyAction `json:"actions,omitempty"`
Icon string `json:"icon,omitempty"` Icon string `json:"icon,omitempty"`
Delay string `json:"delay,omitempty"` Delay string `json:"delay,omitempty"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
} }
// ntfyAction represents an action button in ntfy // ntfyAction represents an action button in ntfy
+7 -7
View File
@@ -30,17 +30,17 @@ type SlackNotifier struct {
// slackMessage represents the Slack API request format // slackMessage represents the Slack API request format
type slackMessage struct { type slackMessage struct {
Channel string `json:"channel,omitempty"` Channel string `json:"channel,omitempty"`
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
IconEmoji string `json:"icon_emoji,omitempty"` IconEmoji string `json:"icon_emoji,omitempty"`
Text string `json:"text,omitempty"` Text string `json:"text,omitempty"`
Blocks []slackBlock `json:"blocks,omitempty"` Blocks []slackBlock `json:"blocks,omitempty"`
Markdown bool `json:"mrkdwn,omitempty"` Markdown bool `json:"mrkdwn,omitempty"`
} }
// slackBlock represents a Slack block element // slackBlock represents a Slack block element
type slackBlock struct { type slackBlock struct {
Type string `json:"type"` Type string `json:"type"`
Text *slackTextBlock `json:"text,omitempty"` Text *slackTextBlock `json:"text,omitempty"`
} }
+9 -9
View File
@@ -8,20 +8,20 @@ import (
"sync" "sync"
"time" "time"
"github.com/igodwin/notifier/internal/domain"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/igodwin/notifier/internal/domain"
) )
// LocalQueue is an in-memory queue implementation // LocalQueue is an in-memory queue implementation
type LocalQueue struct { type LocalQueue struct {
queue chan *domain.QueueMessage queue chan *domain.QueueMessage
messages map[string]*domain.QueueMessage messages map[string]*domain.QueueMessage
mu sync.RWMutex mu sync.RWMutex
config *domain.LocalQueueConfig config *domain.LocalQueueConfig
persistToDisk bool persistToDisk bool
persistPath string persistPath string
closed bool closed bool
closeChan chan struct{} closeChan chan struct{}
} }
// NewLocalQueue creates a new local queue instance // NewLocalQueue creates a new local queue instance