Skip to content

Commit a83729d

Browse files
authored
Merge pull request #36 from YASSERRMD/phase_3
phase 3: structured logging (slog) & X-Request-ID middleware
2 parents e5b2445 + 265c3d5 commit a83729d

3 files changed

Lines changed: 65 additions & 28 deletions

File tree

cmd/siqlah/main.go

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"encoding/hex"
99
"flag"
1010
"fmt"
11-
"log"
11+
"log/slog"
1212
"net/http"
1313
"os"
1414
"os/signal"
@@ -63,6 +63,8 @@ func main() {
6363

6464
_, _ = *oidcClientID, *oidcIssuer // surfaced for future integration
6565

66+
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})))
67+
6668
printBanner()
6769

6870
// Load or generate the operator keypair.
@@ -71,31 +73,33 @@ func main() {
7173
if *operatorKeyHex != "" {
7274
privBytes, err := hex.DecodeString(*operatorKeyHex)
7375
if err != nil || len(privBytes) != ed25519.PrivateKeySize {
74-
log.Fatalf("invalid --operator-key: must be %d-byte hex", ed25519.PrivateKeySize)
76+
slog.Error("invalid --operator-key", "expected_bytes", ed25519.PrivateKeySize)
77+
os.Exit(1)
7578
}
7679
operatorPriv = ed25519.PrivateKey(privBytes)
7780
operatorPub = operatorPriv.Public().(ed25519.PublicKey)
7881
} else {
7982
var err error
8083
operatorPub, operatorPriv, err = ed25519.GenerateKey(rand.Reader)
8184
if err != nil {
82-
log.Fatalf("generate operator key: %v", err)
85+
slog.Error("generate operator key", "error", err)
86+
os.Exit(1)
8387
}
84-
log.Printf("generated operator key (ephemeral — set --operator-key to persist)")
88+
slog.Warn("generated ephemeral operator key — set --operator-key to persist across restarts")
8589
}
86-
log.Printf("operator public key: %s", hex.EncodeToString(operatorPub))
90+
slog.Info("operator key loaded", "public_key", hex.EncodeToString(operatorPub))
8791

8892
// Wire the signing backend.
8993
var receiptSigner signing.Signer
9094
switch *signingBackend {
9195
case "fulcio":
92-
log.Printf("signing backend: fulcio (keyless) — OIDC issuer: %s", *oidcIssuer)
96+
slog.Info("signing backend: fulcio (keyless)", "oidc_issuer", *oidcIssuer)
9397
receiptSigner = signing.NewFulcioSigner(signing.FulcioOptions{
9498
FulcioURL: *fulcioURL,
9599
RekorURL: *rekorURL,
96100
})
97101
default:
98-
log.Printf("signing backend: ed25519")
102+
slog.Info("signing backend: ed25519")
99103
receiptSigner = signing.NewEd25519Signer(operatorPriv)
100104
}
101105
_ = receiptSigner // passed to API in future integration
@@ -108,17 +112,19 @@ func main() {
108112
var builder interface{ BuildAndSign() (*store.Checkpoint, error) }
109113

110114
if *logBackend == "tessera" {
111-
log.Printf("using Tessera backend at %s", *tesseraPath)
115+
slog.Info("using Tessera backend", "path", *tesseraPath)
112116
ts, err := store.NewTesseraStore(ctx, *dbPath, *tesseraPath, *tesseraLogName, operatorPriv)
113117
if err != nil {
114-
log.Fatalf("open tessera store: %v", err)
118+
slog.Error("open tessera store", "error", err)
119+
os.Exit(1)
115120
}
116121
st = ts
117122
builder = checkpoint.NewTesseraBuilder(st, operatorPriv, *maxBatch)
118123
} else {
119124
sqlite, err := store.Open(*dbPath)
120125
if err != nil {
121-
log.Fatalf("open store: %v", err)
126+
slog.Error("open store", "error", err)
127+
os.Exit(1)
122128
}
123129
st = sqlite
124130
builder = checkpoint.NewBuilder(st, operatorPriv, *maxBatch)
@@ -151,9 +157,9 @@ func main() {
151157
case <-ticker.C:
152158
cp, err := builder.BuildAndSign()
153159
if err != nil {
154-
log.Printf("batcher: build checkpoint: %v", err)
160+
slog.Error("batcher: build checkpoint failed", "error", err)
155161
} else if cp != nil {
156-
log.Printf("batcher: checkpoint %d built (tree_size=%d)", cp.ID, cp.TreeSize)
162+
slog.Info("batcher: checkpoint built", "id", cp.ID, "tree_size", cp.TreeSize)
157163
}
158164
}
159165
}
@@ -163,11 +169,12 @@ func main() {
163169
if *rekorAnchor {
164170
ra, err := anchor.NewRekorAnchor(*rekorURL)
165171
if err != nil {
166-
log.Fatalf("create rekor anchor: %v", err)
172+
slog.Error("create rekor anchor", "error", err)
173+
os.Exit(1)
167174
}
168175
sched := anchor.NewAnchorScheduler(ra, st, *rekorInterval)
169176
go sched.Run(ctx)
170-
log.Printf("rekor anchoring enabled: %s every %s", *rekorURL, *rekorInterval)
177+
slog.Info("rekor anchoring enabled", "url", *rekorURL, "interval", *rekorInterval)
171178
}
172179

173180
// Optionally start discrepancy monitor.
@@ -199,18 +206,19 @@ func main() {
199206
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
200207
go func() {
201208
<-sigCh
202-
log.Println("shutting down...")
209+
slog.Info("shutting down...")
203210
cancel()
204211
shutCtx, shutCancel := context.WithTimeout(context.Background(), 10*time.Second)
205212
defer shutCancel()
206213
if err := httpSrv.Shutdown(shutCtx); err != nil {
207-
log.Printf("shutdown: %v", err)
214+
slog.Error("graceful shutdown error", "error", err)
208215
}
209216
}()
210217

211-
log.Printf("siqlah %s (%s) listening on %s", version, commitSHA, *addr)
218+
slog.Info("siqlah listening", "version", version, "commit", commitSHA, "addr", *addr)
212219
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
213-
log.Fatalf("listen: %v", err)
220+
slog.Error("listen failed", "error", err)
221+
os.Exit(1)
214222
}
215223
}
216224

@@ -223,12 +231,12 @@ func parseWitnesses(s string) map[string]ed25519.PublicKey {
223231
for _, pair := range strings.Split(s, ",") {
224232
parts := strings.SplitN(strings.TrimSpace(pair), "=", 2)
225233
if len(parts) != 2 {
226-
log.Printf("warning: invalid witness pair %q, expected id=pubhex", pair)
234+
slog.Warn("invalid witness pair, expected id=pubhex", "pair", pair)
227235
continue
228236
}
229237
b, err := hex.DecodeString(parts[1])
230238
if err != nil || len(b) != ed25519.PublicKeySize {
231-
log.Printf("warning: invalid witness pubkey for %q", parts[0])
239+
slog.Warn("invalid witness pubkey", "id", parts[0])
232240
continue
233241
}
234242
out[parts[0]] = ed25519.PublicKey(b)

internal/api/ingest.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"encoding/hex"
66
"encoding/json"
77
"errors"
8-
"log"
8+
"log/slog"
99
"net/http"
1010
"strconv"
1111
"time"
@@ -208,7 +208,7 @@ func (s *Server) populateModelIdentity(r *vur.Receipt, bundleJSON string) {
208208
bundleJSON, _ = model.ParseBundleFromPEM(bundleJSON)
209209
id, err := model.VerifyModelIdentity(bundleJSON, r.Model, model.VerifyOptions{})
210210
if err != nil {
211-
log.Printf("OMS bundle verification failed for model %q: %v", r.Model, err)
211+
slog.Warn("OMS bundle verification failed", "model", r.Model, "error", err)
212212
return
213213
}
214214
r.ModelSignerIdentity = id.SignerIdentity

internal/api/server.go

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package api
22

33
import (
4+
"context"
45
"crypto/ed25519"
6+
"crypto/rand"
7+
"encoding/hex"
58
"encoding/json"
6-
"log"
9+
"log/slog"
710
"net/http"
811
"time"
912

@@ -15,6 +18,10 @@ import (
1518
"github.com/yasserrmd/siqlah/pkg/vur"
1619
)
1720

21+
type contextKey string
22+
23+
const requestIDKey contextKey = "request_id"
24+
1825
// Server is the HTTP API server for siqlah.
1926
type Server struct {
2027
store store.Store
@@ -147,18 +154,40 @@ func (s *Server) Routes() *http.ServeMux {
147154
return mux
148155
}
149156

150-
// Handler returns the root handler with logging and CORS middleware applied.
157+
// Handler returns the root handler with request-ID, logging, and CORS middleware applied.
151158
func (s *Server) Handler() http.Handler {
152-
return corsMiddleware(loggingMiddleware(s.Routes()))
159+
return corsMiddleware(requestIDMiddleware(loggingMiddleware(s.Routes())))
160+
}
161+
162+
// requestIDMiddleware attaches a unique X-Request-ID to each request context and response header.
163+
func requestIDMiddleware(next http.Handler) http.Handler {
164+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
165+
id := r.Header.Get("X-Request-ID")
166+
if id == "" {
167+
var buf [8]byte
168+
_, _ = rand.Read(buf[:])
169+
id = hex.EncodeToString(buf[:])
170+
}
171+
w.Header().Set("X-Request-ID", id)
172+
ctx := context.WithValue(r.Context(), requestIDKey, id)
173+
next.ServeHTTP(w, r.WithContext(ctx))
174+
})
153175
}
154176

155-
// loggingMiddleware logs each request method, path, and duration.
177+
// loggingMiddleware logs each request method, path, status code, and duration using slog.
156178
func loggingMiddleware(next http.Handler) http.Handler {
157179
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
158180
start := time.Now()
159181
rw := &responseWriter{ResponseWriter: w, code: http.StatusOK}
160182
next.ServeHTTP(rw, r)
161-
log.Printf("%s %s %d %s", r.Method, r.URL.Path, rw.code, time.Since(start))
183+
reqID, _ := r.Context().Value(requestIDKey).(string)
184+
slog.Info("request",
185+
"method", r.Method,
186+
"path", r.URL.Path,
187+
"status", rw.code,
188+
"duration_ms", time.Since(start).Milliseconds(),
189+
"request_id", reqID,
190+
)
162191
})
163192
}
164193

@@ -167,7 +196,7 @@ func corsMiddleware(next http.Handler) http.Handler {
167196
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
168197
w.Header().Set("Access-Control-Allow-Origin", "*")
169198
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
170-
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
199+
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-ID")
171200
if r.Method == http.MethodOptions {
172201
w.WriteHeader(http.StatusNoContent)
173202
return

0 commit comments

Comments
 (0)