feat(core): add configuration, logging and sqlite persistence
DB_PATH, HTTP_ADDR, HTTP_PORT and LOG_LEVEL parsing with validation; slog text logger with a TRACE level and token redaction; embedded idempotent schema on CGO-free SQLite (WAL, busy timeout); token store (create, list, deactivate, authenticate) and append-only audit store with ordered, limited and windowed queries; shared unit test helpers.
This commit is contained in:
1 parent
fe0cd4bf6d
commit
88d7d6d2b9
11 files changed
+2426
No files matched your search
@@ -0,0 +1,111 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// LevelTrace is the custom level below DEBUG
|
||||
const LevelTrace = slog.Level(-8)
|
||||
|
||||
// Names of the supported levels, as printed and as accepted by ParseLogLevel
|
||||
var levelNames = map[slog.Level]string{
|
||||
slog.LevelError: "ERROR",
|
||||
slog.LevelWarn: "WARN",
|
||||
slog.LevelInfo: "INFO",
|
||||
slog.LevelDebug: "DEBUG",
|
||||
LevelTrace: "TRACE",
|
||||
}
|
||||
|
||||
// ParseLogLevel maps a level name (ASCII case-insensitive, exact match) to its slog level
|
||||
func ParseLogLevel(s string) (slog.Level, error) {
|
||||
for level, name := range levelNames {
|
||||
if asciiEqualFold(s, name) {
|
||||
return level, nil
|
||||
}
|
||||
}
|
||||
return 0, errors.New("unknown log level")
|
||||
}
|
||||
|
||||
// LevelName renders a level using the five supported names, falling back to the slog default
|
||||
func LevelName(level slog.Level) string {
|
||||
if name, ok := levelNames[level]; ok {
|
||||
return name
|
||||
}
|
||||
return level.String()
|
||||
}
|
||||
|
||||
// NewLogger builds the text logger writing to w with the given minimum level
|
||||
func NewLogger(w io.Writer, level slog.Level) *slog.Logger {
|
||||
return slog.New(slog.NewTextHandler(w, &slog.HandlerOptions{
|
||||
Level: level,
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
if len(groups) != 0 {
|
||||
return a
|
||||
}
|
||||
switch a.Key {
|
||||
case slog.LevelKey:
|
||||
// Render the level with the custom names (TRACE instead of DEBUG-4)
|
||||
if level, ok := a.Value.Any().(slog.Level); ok {
|
||||
a.Value = slog.StringValue(LevelName(level))
|
||||
}
|
||||
case slog.TimeKey:
|
||||
// Always log timestamps in UTC
|
||||
if a.Value.Kind() == slog.KindTime {
|
||||
a.Value = slog.TimeValue(a.Value.Time().UTC())
|
||||
}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
// trace emits a TRACE record
|
||||
func trace(ctx context.Context, log *slog.Logger, msg string, args ...any) {
|
||||
log.Log(ctx, LevelTrace, msg, args...)
|
||||
}
|
||||
|
||||
// traceFunc emits a TRACE function-entry record and returns the matching function-exit emitter
|
||||
// Usage: defer traceFunc(ctx, log, "Name")()
|
||||
func traceFunc(ctx context.Context, log *slog.Logger, fn string) func() {
|
||||
trace(ctx, log, "function entry", "func", fn)
|
||||
return func() {
|
||||
trace(ctx, log, "function exit", "func", fn)
|
||||
}
|
||||
}
|
||||
|
||||
// tokenID returns a stable, non-reversible identifier for a token, safe to log
|
||||
// It is the first 8 hex characters of the SHA-256 digest of the token
|
||||
func tokenID(token string) string {
|
||||
sum := sha256.Sum256([]byte(token))
|
||||
return hex.EncodeToString(sum[:4])
|
||||
}
|
||||
|
||||
// asciiEqualFold compares two strings case-insensitively over ASCII letters only
|
||||
// Non-ASCII bytes must match exactly, so Unicode case folding never applies
|
||||
func asciiEqualFold(a, b string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(a); i++ {
|
||||
if asciiLower(a[i]) != asciiLower(b[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// asciiLower lowercases a single ASCII letter
|
||||
func asciiLower(c byte) byte {
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
return c + ('a' - 'A')
|
||||
}
|
||||
return c
|
||||
}
|
||||
Reference in new issue
Block a user