chore: remove dead code and unused exports

Remove ~900 lines of unused code identified by static analysis:

Go:
- internal/logging: Remove 10 unused functions (InitFromConfig, New,
  FromContext, WithLogger, etc.) that were built but never integrated
- cmd/pulse-sensor-proxy: Remove 7 dead validation functions for a
  removed command execution feature
- internal/metrics: Remove 8 unused notification metric functions and
  10 Prometheus metrics that were never wired up

Frontend:
- Delete ActivationBanner.tsx stub component
- Remove unused exports: stopMetricsSampler, getSamplerStatus,
  formatSpeedCompact, parseMetricKey, getResourceAlerts
This commit is contained in:
rcourtman 2025-11-27 13:17:39 +00:00
parent 244431b6c2
commit f7ffb36c41
12 changed files with 116 additions and 993 deletions

View file

@ -9,8 +9,6 @@ import (
"strings"
"sync"
"time"
"unicode"
"unicode/utf8"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
@ -25,13 +23,6 @@ var (
ipv4Regex = regexp.MustCompile(`^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$`)
)
var (
allowedCommands = map[string]struct{}{
"sensors": {},
"ipmitool": {},
}
)
// sanitizeCorrelationID validates and sanitizes a correlation ID
// Returns a valid UUID, generating a new one if input is missing or invalid
func sanitizeCorrelationID(id string) string {
@ -70,142 +61,6 @@ func validateNodeName(name string) error {
return fmt.Errorf("invalid node name")
}
func validateCommand(name string, args []string) error {
if err := validateCommandName(name); err != nil {
return err
}
for _, arg := range args {
if err := validateCommandArg(arg); err != nil {
return err
}
}
if name == "ipmitool" {
if err := validateIPMIToolArgs(args); err != nil {
return err
}
}
return nil
}
func validateCommandName(name string) error {
if name == "" {
return errors.New("command required")
}
if strings.Contains(name, "/") {
return errors.New("absolute command paths not allowed")
}
if _, ok := allowedCommands[name]; !ok {
return fmt.Errorf("command %q not permitted", name)
}
if !isASCII(name) {
return errors.New("command must be ASCII")
}
return nil
}
func validateCommandArg(arg string) error {
if len(arg) == 0 {
return nil
}
if len(arg) > 1024 {
return errors.New("argument too long")
}
if !utf8.ValidString(arg) {
return errors.New("argument contains invalid UTF-8")
}
if hasNullByte(arg) {
return errors.New("argument contains null byte")
}
if !isASCII(arg) {
return errors.New("argument must be ASCII")
}
if hasShellMeta(arg) {
return errors.New("argument contains forbidden shell characters")
}
if strings.Contains(arg, "=") && !strings.HasPrefix(arg, "-") {
return errors.New("environment-style arguments not permitted")
}
return nil
}
func validateIPMIToolArgs(args []string) error {
lowered := make([]string, len(args))
for i, arg := range args {
lowered[i] = strings.ToLower(arg)
}
for i := 0; i < len(lowered); i++ {
token := lowered[i]
switch token {
case "shell", "raw", "exec", "lanplus", "lanplusciphers":
return errors.New("dangerous ipmitool arguments not permitted")
case "chassis":
if i+1 < len(lowered) {
switch lowered[i+1] {
case "power", "bootparam", "status", "policy":
return errors.New("chassis operations not permitted")
}
}
case "power", "reset", "off", "cycle", "bmc", "mc":
return errors.New("power control commands not permitted")
}
}
return nil
}
func hasShellMeta(s string) bool {
forbidden := []string{";", "|", "&", "$", "`", "\\", ">", "<", "(", ")", "[", "]", "{", "}", "!", "~"}
for _, ch := range forbidden {
if strings.Contains(s, ch) {
return true
}
}
if strings.Contains(s, "..") {
return true
}
if strings.ContainsAny(s, "\n\r\t") {
return true
}
if strings.HasPrefix(s, "-") && strings.Contains(s, "=") {
if strings.Contains(s, "/") {
return true
}
}
return false
}
func hasNullByte(s string) bool {
return strings.IndexByte(s, 0) >= 0
}
func isASCII(s string) bool {
for _, r := range s {
if r > unicode.MaxASCII {
return false
}
}
return true
}
const (
nodeValidatorCacheTTL = 5 * time.Minute

View file

@ -1,31 +0,0 @@
package main
import (
"strings"
"testing"
)
func FuzzValidateCommand(f *testing.F) {
seeds := []string{
"sensors -j",
"ipmitool sdr",
"sensors",
"ipmitool lan print",
}
for _, seed := range seeds {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, input string) {
fields := strings.Fields(input)
if len(fields) == 0 {
return
}
cmd := fields[0]
args := []string{}
if len(fields) > 1 {
args = fields[1:]
}
validateCommand(cmd, args) // ensure no panics
})
}

View file

@ -83,48 +83,6 @@ func TestValidateNodeName(t *testing.T) {
}
}
func TestValidateCommand(t *testing.T) {
type tc struct {
name string
args []string
wantErr bool
desc string
}
cases := []tc{
{name: "sensors", args: nil, wantErr: false, desc: "bare sensors"},
{name: "sensors", args: []string{"-j"}, wantErr: false, desc: "json flag"},
{name: "ipmitool", args: []string{"sdr"}, wantErr: false, desc: "safe ipmitool"},
{name: "sensors", args: []string{"; rm -rf /"}, wantErr: true, desc: "shell metachar"},
{name: "sensors", args: []string{"$(id)"}, wantErr: true, desc: "subshell"},
{name: "ipmitool", args: []string{"-H", "1.2.3.4", "&&", "shutdown"}, wantErr: true, desc: "command chaining"},
{name: "sensors", args: []string{">/tmp/out"}, wantErr: true, desc: "redirect"},
{name: "senso\u200Brs", wantErr: true, desc: "unicode homoglyph"},
{name: "sensors", args: []string{"-" + strings.Repeat("v", 2000)}, wantErr: true, desc: "arg too long"},
{name: "sensors", args: []string{"test\x00"}, wantErr: true, desc: "null byte arg"},
{name: "ipmitool", args: []string{"chassis", "power", "off"}, wantErr: true, desc: "dangerous ipmitool"},
{name: "sensors", args: []string{"LC_ALL=C"}, wantErr: true, desc: "env prefix"},
{name: "/usr/bin/sensors", wantErr: true, desc: "absolute path"},
{name: "ipmitool", args: []string{"--extraneous=../../etc/passwd"}, wantErr: true, desc: "path traversal"},
}
for _, tc := range cases {
tc := tc
if tc.desc == "" {
tc.desc = tc.name
}
t.Run(tc.desc, func(t *testing.T) {
err := validateCommand(tc.name, tc.args)
if tc.wantErr && err == nil {
t.Fatalf("expected error for %s %v", tc.name, tc.args)
}
if !tc.wantErr && err != nil {
t.Fatalf("unexpected error for %s %v: %v", tc.name, tc.args, err)
}
})
}
}
type stubResolver struct {
ips []net.IP
err error

View file

@ -1,18 +0,0 @@
import type { JSX } from 'solid-js';
import type { Alert } from '@/types/api';
import type { ActivationState, AlertConfig } from '@/types/alerts';
interface ActivationBannerProps {
activationState: () => ActivationState | null;
activeAlerts: () => Alert[] | undefined;
config: () => AlertConfig | null;
isPastObservationWindow: () => boolean;
isLoading: () => boolean;
refreshActiveAlerts: () => Promise<void>;
activate: () => Promise<boolean>;
}
export function ActivationBanner(_props: ActivationBannerProps): JSX.Element {
// Notifications activation banner/modal intentionally disabled.
return <></>;
}

View file

@ -156,25 +156,3 @@ export function startMetricsSampler(): void {
}
}
/**
* Stop the metrics sampler
*/
export function stopMetricsSampler(): void {
if (samplerInterval !== null) {
window.clearInterval(samplerInterval);
samplerInterval = null;
}
isRunning = false;
logger.info('[MetricsSampler] Stopped');
}
/**
* Get sampler status
*/
export function getSamplerStatus() {
return {
isRunning,
intervalMs: SAMPLE_INTERVAL_MS,
};
}

View file

@ -106,12 +106,3 @@ export const getAlertStyles = (
};
};
// Get alert messages for a specific resource
export const getResourceAlerts = (
resourceId: string,
activeAlerts: Record<string, Alert>,
alertsEnabled: boolean | undefined = isAlertsActivationEnabled(),
): Alert[] => {
if (!alertsEnabled) return [];
return Object.values(activeAlerts).filter((alert) => alert.resourceId === resourceId);
};

View file

@ -15,15 +15,6 @@ export function formatSpeed(bytesPerSecond: number, decimals = 0): string {
return `${formatBytes(bytesPerSecond, decimals)}/s`;
}
export function formatSpeedCompact(bytesPerSecond: number): string {
if (!bytesPerSecond || bytesPerSecond < 0) return '0';
const k = 1024;
const mbps = bytesPerSecond / (k * k);
if (mbps < 1) return '<1';
if (mbps < 10) return mbps.toFixed(1);
return Math.round(mbps).toString();
}
export function formatPercent(value: number): string {
if (!Number.isFinite(value)) return '0%';
const abs = Math.abs(value);

View file

@ -17,20 +17,6 @@ export function buildMetricKey(kind: MetricResourceKind, id: string): string {
return `${kind}:${id}`;
}
/**
* Parse a metric key back into its components
* Returns null if the key format is invalid
*/
export function parseMetricKey(key: string): { kind: MetricResourceKind; id: string } | null {
const colonIndex = key.indexOf(':');
if (colonIndex === -1) return null;
const kind = key.slice(0, colonIndex) as MetricResourceKind;
const id = key.slice(colonIndex + 1);
return { kind, id };
}
/**
* Extract the prefix from a metric resource kind
* Used for bulk operations on a specific resource type

View file

@ -7,7 +7,6 @@ import (
"io"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
@ -23,7 +22,6 @@ type ctxKey string
const (
requestIDKey ctxKey = "logging_request_id"
loggerKey ctxKey = "logging_logger"
)
// Config controls logger initialization.
@ -37,15 +35,6 @@ type Config struct {
Compress bool // gzip rotated logs
}
// Option customizes logger construction.
type Option func(*options)
type options struct {
writer io.Writer
fields map[string]interface{}
withCaller bool
}
var (
mu sync.RWMutex
baseLogger zerolog.Logger
@ -90,148 +79,11 @@ func Init(cfg Config) zerolog.Logger {
return baseLogger
}
// InitFromConfig initialises logging with environment overrides.
func InitFromConfig(ctx context.Context, cfg Config) (zerolog.Logger, error) {
if cfg.Level == "" {
cfg.Level = "info"
}
if cfg.Format == "" {
cfg.Format = "auto"
}
if envLevel := os.Getenv("LOG_LEVEL"); envLevel != "" {
cfg.Level = envLevel
}
if envFormat := os.Getenv("LOG_FORMAT"); envFormat != "" {
cfg.Format = envFormat
}
if envFile := os.Getenv("LOG_FILE"); envFile != "" {
cfg.FilePath = envFile
}
if envSize := os.Getenv("LOG_MAX_SIZE"); envSize != "" {
if size, err := strconv.Atoi(envSize); err == nil {
cfg.MaxSizeMB = size
}
}
if envAge := os.Getenv("LOG_MAX_AGE"); envAge != "" {
if age, err := strconv.Atoi(envAge); err == nil {
cfg.MaxAgeDays = age
}
}
if envCompress := os.Getenv("LOG_COMPRESS"); envCompress != "" {
switch strings.ToLower(strings.TrimSpace(envCompress)) {
case "0", "false", "no":
cfg.Compress = false
default:
cfg.Compress = true
}
}
if !isValidLevel(cfg.Level) {
return zerolog.Logger{}, fmt.Errorf("invalid log level %q: must be debug, info, warn, or error", cfg.Level)
}
format := strings.ToLower(strings.TrimSpace(cfg.Format))
if format != "" && format != "json" && format != "console" && format != "auto" {
return zerolog.Logger{}, fmt.Errorf("invalid log format %q: must be json, console, or auto", cfg.Format)
}
logger := Init(cfg)
return logger, nil
}
// IsLevelEnabled reports whether the provided level is enabled for logging.
func IsLevelEnabled(level zerolog.Level) bool {
return level >= zerolog.GlobalLevel()
}
// New creates a logger tailored to a specific component.
func New(component string, opts ...Option) zerolog.Logger {
cfg := collectOptions(opts...)
mu.RLock()
globalWriter := baseWriter
globalComponent := baseComponent
mu.RUnlock()
writer := cfg.writer
if writer == nil {
writer = globalWriter
}
component = strings.TrimSpace(component)
if component == "" {
component = globalComponent
}
logger := zerolog.New(writer)
contextBuilder := logger.With().Timestamp()
if component != "" {
contextBuilder = contextBuilder.Str("component", component)
}
if len(cfg.fields) > 0 {
contextBuilder = contextBuilder.Fields(cfg.fields)
}
if cfg.withCaller {
contextBuilder = contextBuilder.Caller()
}
return contextBuilder.Logger()
}
// WithCaller enables caller logging.
func WithCaller() Option {
return func(o *options) {
o.withCaller = true
}
}
// WithWriter overrides the logger writer.
func WithWriter(w io.Writer) Option {
return func(o *options) {
if w != nil {
o.writer = w
}
}
}
// WithFields adds static fields to the logger.
func WithFields(fields map[string]interface{}) Option {
return func(o *options) {
if len(fields) == 0 {
return
}
if o.fields == nil {
o.fields = make(map[string]interface{}, len(fields))
}
for k, v := range fields {
o.fields[k] = v
}
}
}
// FromContext returns a logger enriched with context metadata.
func FromContext(ctx context.Context) zerolog.Logger {
if ctx == nil {
return getBaseLogger()
}
if existing, ok := ctx.Value(loggerKey).(zerolog.Logger); ok {
return enrichWithRequestID(existing, ctx)
}
return enrichWithRequestID(getBaseLogger(), ctx)
}
// WithLogger stores a logger on the context.
func WithLogger(ctx context.Context, logger zerolog.Logger) context.Context {
if ctx == nil {
ctx = context.Background()
}
return context.WithValue(ctx, loggerKey, logger)
}
// WithRequestID stores (or generates) a request ID on the context.
func WithRequestID(ctx context.Context, id string) (context.Context, string) {
if ctx == nil {
@ -244,27 +96,6 @@ func WithRequestID(ctx context.Context, id string) (context.Context, string) {
return context.WithValue(ctx, requestIDKey, id), id
}
// GetRequestID retrieves the request ID from context.
func GetRequestID(ctx context.Context) string {
if ctx == nil {
return ""
}
if id, ok := ctx.Value(requestIDKey).(string); ok {
return id
}
return ""
}
func collectOptions(opts ...Option) options {
cfg := options{}
for _, opt := range opts {
if opt != nil {
opt(&cfg)
}
}
return cfg
}
func parseLevel(level string) zerolog.Level {
switch strings.ToLower(strings.TrimSpace(level)) {
case "debug":
@ -278,15 +109,6 @@ func parseLevel(level string) zerolog.Level {
}
}
func isValidLevel(level string) bool {
switch strings.ToLower(strings.TrimSpace(level)) {
case "debug", "info", "warn", "error":
return true
default:
return false
}
}
func selectWriter(format string) io.Writer {
format = strings.ToLower(strings.TrimSpace(format))
switch format {
@ -318,22 +140,6 @@ func isTerminal(file *os.File) bool {
return term.IsTerminal(int(file.Fd()))
}
func getBaseLogger() zerolog.Logger {
mu.RLock()
defer mu.RUnlock()
return baseLogger
}
func enrichWithRequestID(logger zerolog.Logger, ctx context.Context) zerolog.Logger {
if ctx == nil {
return logger
}
if id := GetRequestID(ctx); id != "" {
return logger.With().Str("request_id", id).Logger()
}
return logger
}
type rollingFileWriter struct {
mu sync.Mutex
path string

View file

@ -2,12 +2,8 @@ package logging
import (
"bytes"
"context"
"encoding/json"
"io"
"os"
"reflect"
"strings"
"sync"
"testing"
@ -27,24 +23,6 @@ func resetLoggingState() {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
}
func readJSONLine(t *testing.T, buf *bytes.Buffer) map[string]interface{} {
t.Helper()
line := strings.TrimSpace(buf.String())
if idx := strings.IndexByte(line, '\n'); idx >= 0 {
line = line[:idx]
}
if line == "" {
t.Fatalf("expected log output, got empty string")
}
var event map[string]interface{}
if err := json.Unmarshal([]byte(line), &event); err != nil {
t.Fatalf("failed to unmarshal log line: %v", err)
}
return event
}
func TestInitJSONFormatSetsLevelAndComponent(t *testing.T) {
t.Cleanup(resetLoggingState)
@ -118,58 +96,7 @@ func TestInitAutoFormatWithPipe(t *testing.T) {
}
}
func TestNewLoggerWithComponentAndFields(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "info",
Component: "root",
})
var buf bytes.Buffer
logger := New("worker", WithWriter(&buf), WithFields(map[string]interface{}{
"request": "sync",
}))
logger.Info().Msg("processing")
event := readJSONLine(t, &buf)
if event["component"] != "worker" {
t.Fatalf("expected component worker, got %v", event["component"])
}
if event["request"] != "sync" {
t.Fatalf("expected request field, got %v", event["request"])
}
if event["level"] != "info" {
t.Fatalf("expected level info, got %v", event["level"])
}
if event["message"] != "processing" {
t.Fatalf("expected message processing, got %v", event["message"])
}
}
func TestNewLoggerInheritsComponentWhenEmpty(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "info",
Component: "core",
})
var buf bytes.Buffer
logger := New("", WithWriter(&buf))
logger.Warn().Msg("warn")
event := readJSONLine(t, &buf)
if event["component"] != "core" {
t.Fatalf("expected inherited component core, got %v", event["component"])
}
}
func TestNewLoggerWithCaller(t *testing.T) {
func TestWithRequestID(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
@ -177,104 +104,21 @@ func TestNewLoggerWithCaller(t *testing.T) {
Level: "info",
})
var buf bytes.Buffer
logger := New("svc", WithWriter(&buf), WithCaller())
logger.Error().Msg("boom")
event := readJSONLine(t, &buf)
caller, ok := event["caller"].(string)
if !ok || !strings.Contains(caller, "logging_test.go") {
t.Fatalf("expected caller information, got %v", event["caller"])
}
}
func TestNewLoggerWithCustomWriter(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "info",
})
var buf bytes.Buffer
logger := New("custom", WithWriter(&buf))
logger.Info().Msg("hello")
if buf.Len() == 0 {
t.Fatal("expected output on custom writer")
}
}
func TestContextHelpersWithRequestID(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "info",
})
ctx := context.Background()
ctx, generated := WithRequestID(ctx, "")
ctx, generated := WithRequestID(nil, "")
if generated == "" {
t.Fatal("expected generated request id")
}
if got := GetRequestID(ctx); got != generated {
t.Fatalf("expected stored request id %s, got %s", generated, got)
}
var buf bytes.Buffer
logger := New("api", WithWriter(&buf))
ctx = WithLogger(ctx, logger)
info := FromContext(ctx)
info.Info().Msg("ctx-log")
event := readJSONLine(t, &buf)
if event["request_id"] != generated {
t.Fatalf("expected request_id %s, got %v", generated, event["request_id"])
}
}
func TestContextHelpersWithExistingLogger(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "debug",
})
var buf bytes.Buffer
base := New("svc", WithWriter(&buf))
ctx := WithLogger(context.Background(), base)
ctx, id := WithRequestID(ctx, "custom-123")
logger := FromContext(ctx)
logger.Debug().Msg("debug")
event := readJSONLine(t, &buf)
if event["component"] != "svc" {
t.Fatalf("expected component svc, got %v", event["component"])
}
if event["request_id"] != "custom-123" {
t.Fatalf("expected request_id custom-123, got %v", event["request_id"])
}
if event["level"] != "debug" {
t.Fatalf("expected level debug, got %v", event["level"])
}
if id != "custom-123" {
t.Fatalf("expected returned id to match input, got %s", id)
}
}
func TestWithLoggerNilContext(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{})
ctx := WithLogger(context.Background(), New("svc", WithWriter(io.Discard)))
if ctx == nil {
t.Fatal("expected non-nil context")
}
ctx2, id := WithRequestID(nil, "custom-123")
if id != "custom-123" {
t.Fatalf("expected custom-123, got %s", id)
}
if ctx2 == nil {
t.Fatal("expected non-nil context")
}
}
func TestWithRequestIDTrimsWhitespace(t *testing.T) {
@ -282,56 +126,10 @@ func TestWithRequestIDTrimsWhitespace(t *testing.T) {
Init(Config{})
ctx, id := WithRequestID(context.Background(), " ")
_, id := WithRequestID(nil, " ")
if id == "" {
t.Fatal("expected generated id for whitespace input")
}
if GetRequestID(ctx) != id {
t.Fatalf("expected context request id %s, got %s", id, GetRequestID(ctx))
}
}
func TestFromContextWithoutRequestID(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "info",
})
var buf bytes.Buffer
mu.Lock()
baseLogger = zerolog.New(&buf).With().Timestamp().Logger()
baseWriter = &buf
baseComponent = ""
log.Logger = baseLogger
mu.Unlock()
base := FromContext(context.Background())
base.Info().Msg("no-request")
event := readJSONLine(t, &buf)
if _, ok := event["request_id"]; ok {
t.Fatalf("did not expect request_id, got %v", event["request_id"])
}
}
func TestNewLoggerWithoutComponentOmitsField(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "info",
})
var buf bytes.Buffer
logger := New("", WithWriter(&buf))
logger.Info().Msg("no-component")
event := readJSONLine(t, &buf)
if _, exists := event["component"]; exists {
t.Fatalf("did not expect component field, got %v", event["component"])
}
}
func TestInitThreadSafety(t *testing.T) {
@ -364,91 +162,6 @@ func TestInitThreadSafety(t *testing.T) {
}
}
func TestInitFromConfigWithDefaults(t *testing.T) {
t.Cleanup(resetLoggingState)
logger, err := InitFromConfig(context.Background(), Config{
Component: "test",
})
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if reflect.DeepEqual(logger, zerolog.Logger{}) {
t.Fatal("expected initialized logger")
}
if zerolog.GlobalLevel() != zerolog.InfoLevel {
t.Fatalf("expected default info level, got %s", zerolog.GlobalLevel())
}
}
func TestInitFromConfigWithEnvOverrides(t *testing.T) {
t.Cleanup(resetLoggingState)
t.Cleanup(func() {
os.Unsetenv("LOG_LEVEL")
os.Unsetenv("LOG_FORMAT")
})
os.Setenv("LOG_LEVEL", "debug")
os.Setenv("LOG_FORMAT", "json")
logger, err := InitFromConfig(context.Background(), Config{
Level: "info",
Format: "console",
Component: "test",
})
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if reflect.DeepEqual(logger, zerolog.Logger{}) {
t.Fatal("expected initialized logger")
}
// Env override should set debug level
if zerolog.GlobalLevel() != zerolog.DebugLevel {
t.Fatalf("expected debug level from env override, got %s", zerolog.GlobalLevel())
}
// Format should be JSON (from env)
mu.RLock()
defer mu.RUnlock()
if _, ok := baseWriter.(zerolog.ConsoleWriter); ok {
t.Fatal("expected JSON writer from env override, got console writer")
}
}
func TestInitFromConfigInvalidLevel(t *testing.T) {
t.Cleanup(resetLoggingState)
_, err := InitFromConfig(context.Background(), Config{
Level: "invalid",
Format: "json",
})
if err == nil {
t.Fatal("expected error for invalid level")
}
if !strings.Contains(err.Error(), "invalid log level") {
t.Fatalf("expected invalid level error, got %v", err)
}
}
func TestInitFromConfigInvalidFormat(t *testing.T) {
t.Cleanup(resetLoggingState)
_, err := InitFromConfig(context.Background(), Config{
Level: "info",
Format: "invalid",
})
if err == nil {
t.Fatal("expected error for invalid format")
}
if !strings.Contains(err.Error(), "invalid log format") {
t.Fatalf("expected invalid format error, got %v", err)
}
}
func TestIsLevelEnabled(t *testing.T) {
t.Cleanup(resetLoggingState)
@ -474,3 +187,108 @@ func TestIsLevelEnabled(t *testing.T) {
t.Fatal("expected debug level to be enabled after setting global level")
}
}
func TestRollingFileWriter(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
logFile := dir + "/test.log"
cfg := Config{
Format: "json",
Level: "info",
FilePath: logFile,
MaxSizeMB: 1,
MaxAgeDays: 7,
Compress: false,
}
Init(cfg)
// Write some log output
log.Info().Msg("test message")
// Check file exists (this confirms rolling file writer was created)
if _, err := os.Stat(logFile); os.IsNotExist(err) {
t.Fatal("expected log file to be created")
}
// Check file has content
data, err := os.ReadFile(logFile)
if err != nil {
t.Fatalf("failed to read log file: %v", err)
}
if len(data) == 0 {
t.Fatal("expected log file to have content")
}
}
func TestParseLevelDefaults(t *testing.T) {
tests := []struct {
input string
want zerolog.Level
}{
{"debug", zerolog.DebugLevel},
{"DEBUG", zerolog.DebugLevel},
{"info", zerolog.InfoLevel},
{"INFO", zerolog.InfoLevel},
{"warn", zerolog.WarnLevel},
{"WARN", zerolog.WarnLevel},
{"error", zerolog.ErrorLevel},
{"ERROR", zerolog.ErrorLevel},
{"unknown", zerolog.InfoLevel},
{"", zerolog.InfoLevel},
}
for _, tc := range tests {
got := parseLevel(tc.input)
if got != tc.want {
t.Errorf("parseLevel(%q) = %v, want %v", tc.input, got, tc.want)
}
}
}
func TestSelectWriter(t *testing.T) {
tests := []struct {
format string
isTTY bool
wantErr bool
}{
{"json", false, false},
{"console", false, false},
{"auto", false, false},
}
for _, tc := range tests {
w := selectWriter(tc.format)
if w == nil {
t.Errorf("selectWriter(%q) returned nil", tc.format)
}
}
}
// Test that the logging package doesn't panic under concurrent use
func TestConcurrentLogging(t *testing.T) {
t.Cleanup(resetLoggingState)
var buf bytes.Buffer
mu.Lock()
baseWriter = &buf
baseLogger = zerolog.New(&buf).With().Timestamp().Logger()
log.Logger = baseLogger
mu.Unlock()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
log.Info().Int("iteration", n).Msg("concurrent log")
}(i)
}
wg.Wait()
if buf.Len() == 0 {
t.Fatal("expected log output from concurrent logging")
}
}

View file

@ -1,8 +1,6 @@
package metrics
import (
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
@ -50,87 +48,6 @@ var (
[]string{"type"},
)
// Notification metrics
NotificationsSentTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "pulse_notifications_sent_total",
Help: "Total number of notifications sent by method and status",
},
[]string{"method", "status"}, // method: email, webhook, apprise; status: success, failure
)
NotificationQueueDepth = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "pulse_notification_queue_depth",
Help: "Number of notifications in queue by status",
},
[]string{"status"}, // pending, sending, dlq
)
NotificationDeliveryDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "pulse_notification_delivery_duration_seconds",
Help: "Time to deliver notifications",
Buckets: prometheus.DefBuckets,
},
[]string{"method"},
)
NotificationRetriesTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "pulse_notification_retries_total",
Help: "Total number of notification retry attempts",
},
[]string{"method"},
)
NotificationDLQTotal = promauto.NewCounter(
prometheus.CounterOpts{
Name: "pulse_notification_dlq_total",
Help: "Total number of notifications moved to dead letter queue",
},
)
// Alert grouping metrics
NotificationsGroupedTotal = promauto.NewCounter(
prometheus.CounterOpts{
Name: "pulse_notifications_grouped_total",
Help: "Total number of grouped notifications sent",
},
)
AlertsGroupedCount = promauto.NewHistogram(
prometheus.HistogramOpts{
Name: "pulse_alerts_grouped_count",
Help: "Number of alerts in grouped notifications",
Buckets: []float64{1, 2, 5, 10, 20, 50, 100},
},
)
// Escalation metrics
AlertEscalationsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "pulse_alert_escalations_total",
Help: "Total number of alert escalations by level",
},
[]string{"level"},
)
// History metrics
AlertHistorySize = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "pulse_alert_history_size",
Help: "Number of alerts in history",
},
)
AlertHistorySaveErrors = promauto.NewCounter(
prometheus.CounterOpts{
Name: "pulse_alert_history_save_errors_total",
Help: "Total number of alert history save failures",
},
)
// Suppression and quiet hours metrics
AlertsSuppressedTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
@ -169,52 +86,7 @@ func RecordAlertAcknowledged() {
AlertsAcknowledgedTotal.Inc()
}
// RecordNotificationSent records a notification delivery attempt
func RecordNotificationSent(method string, success bool) {
status := "success"
if !success {
status = "failure"
}
NotificationsSentTotal.WithLabelValues(method, status).Inc()
}
// RecordNotificationRetry records a notification retry
func RecordNotificationRetry(method string) {
NotificationRetriesTotal.WithLabelValues(method).Inc()
}
// RecordNotificationDLQ records a notification moved to DLQ
func RecordNotificationDLQ() {
NotificationDLQTotal.Inc()
}
// RecordGroupedNotification records a grouped notification
func RecordGroupedNotification(alertCount int) {
NotificationsGroupedTotal.Inc()
AlertsGroupedCount.Observe(float64(alertCount))
}
// RecordAlertEscalation records an alert escalation
func RecordAlertEscalation(level int) {
AlertEscalationsTotal.WithLabelValues(fmt.Sprintf("%d", level)).Inc()
}
// RecordAlertSuppressed records a suppressed alert
func RecordAlertSuppressed(reason string) {
AlertsSuppressedTotal.WithLabelValues(reason).Inc()
}
// UpdateQueueDepth updates the notification queue depth metric
func UpdateQueueDepth(status string, count int) {
NotificationQueueDepth.WithLabelValues(status).Set(float64(count))
}
// UpdateHistorySize updates the alert history size metric
func UpdateHistorySize(size int) {
AlertHistorySize.Set(float64(size))
}
// RecordHistorySaveError records a history save failure
func RecordHistorySaveError() {
AlertHistorySaveErrors.Inc()
}

View file

@ -38,40 +38,6 @@ func TestRecordAlertAcknowledged(t *testing.T) {
RecordAlertAcknowledged()
}
func TestRecordNotificationSent_Success(t *testing.T) {
// Should not panic
RecordNotificationSent("email", true)
}
func TestRecordNotificationSent_Failure(t *testing.T) {
// Should not panic
RecordNotificationSent("webhook", false)
}
func TestRecordNotificationRetry(t *testing.T) {
// Should not panic
RecordNotificationRetry("email")
}
func TestRecordNotificationDLQ(t *testing.T) {
// Should not panic
RecordNotificationDLQ()
}
func TestRecordGroupedNotification(t *testing.T) {
// Should not panic with various counts
RecordGroupedNotification(1)
RecordGroupedNotification(5)
RecordGroupedNotification(50)
}
func TestRecordAlertEscalation(t *testing.T) {
// Should not panic with various levels
RecordAlertEscalation(1)
RecordAlertEscalation(2)
RecordAlertEscalation(3)
}
func TestRecordAlertSuppressed(t *testing.T) {
// Should not panic with various reasons
RecordAlertSuppressed("quiet_hours")
@ -79,25 +45,6 @@ func TestRecordAlertSuppressed(t *testing.T) {
RecordAlertSuppressed("duplicate")
}
func TestUpdateQueueDepth(t *testing.T) {
// Should not panic with various statuses and counts
UpdateQueueDepth("pending", 10)
UpdateQueueDepth("sending", 5)
UpdateQueueDepth("dlq", 0)
}
func TestUpdateHistorySize(t *testing.T) {
// Should not panic with various sizes
UpdateHistorySize(0)
UpdateHistorySize(100)
UpdateHistorySize(1000)
}
func TestRecordHistorySaveError(t *testing.T) {
// Should not panic
RecordHistorySaveError()
}
func TestMetricVectors_NotNil(t *testing.T) {
// Verify that metric vectors are properly initialized
if AlertsActive == nil {
@ -115,36 +62,6 @@ func TestMetricVectors_NotNil(t *testing.T) {
if AlertDurationSeconds == nil {
t.Error("AlertDurationSeconds should not be nil")
}
if NotificationsSentTotal == nil {
t.Error("NotificationsSentTotal should not be nil")
}
if NotificationQueueDepth == nil {
t.Error("NotificationQueueDepth should not be nil")
}
if NotificationDeliveryDuration == nil {
t.Error("NotificationDeliveryDuration should not be nil")
}
if NotificationRetriesTotal == nil {
t.Error("NotificationRetriesTotal should not be nil")
}
if NotificationDLQTotal == nil {
t.Error("NotificationDLQTotal should not be nil")
}
if NotificationsGroupedTotal == nil {
t.Error("NotificationsGroupedTotal should not be nil")
}
if AlertsGroupedCount == nil {
t.Error("AlertsGroupedCount should not be nil")
}
if AlertEscalationsTotal == nil {
t.Error("AlertEscalationsTotal should not be nil")
}
if AlertHistorySize == nil {
t.Error("AlertHistorySize should not be nil")
}
if AlertHistorySaveErrors == nil {
t.Error("AlertHistorySaveErrors should not be nil")
}
if AlertsSuppressedTotal == nil {
t.Error("AlertsSuppressedTotal should not be nil")
}