Add logging service
New Go program (logger/) that: - Accepts POSTed JSON log messages via POST /log - Stores last N messages in a ring buffer (default 1M) - Retrieves logs via GET /logs with limit/before/after filters - Shows status via GET /status Also updates express/logging.ts to POST messages to the logger service. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,15 +32,39 @@ type FilterArgument = {
|
|||||||
match?: (string | RegExp)[];
|
match?: (string | RegExp)[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const log = (_message: Message) => {
|
const loggerUrl = `http://${cli.logAddress.host}:${cli.logAddress.port}`;
|
||||||
// WRITEME
|
|
||||||
console.log(
|
const log = (message: Message) => {
|
||||||
`will POST a message to ${cli.logAddress.host}:${cli.logAddress.port}`,
|
const payload = {
|
||||||
);
|
timestamp: message.timestamp ?? Date.now(),
|
||||||
|
source: message.source,
|
||||||
|
text: message.text,
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLogs = (filter: FilterArgument) => {
|
fetch(`${loggerUrl}/log`, {
|
||||||
// WRITEME
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error("[logging] Failed to send log:", err.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLogs = async (filter: FilterArgument): Promise<Message[]> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filter.limit) {
|
||||||
|
params.set("limit", String(filter.limit));
|
||||||
|
}
|
||||||
|
if (filter.before) {
|
||||||
|
- params.set("before", String(filter.before));
|
||||||
|
}
|
||||||
|
if (filter.after) {
|
||||||
|
params.set("after", String(filter.after));
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${loggerUrl}/logs?${params.toString()}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME: there's scope for more specialized functions although they
|
// FIXME: there's scope for more specialized functions although they
|
||||||
|
|||||||
1
logger/.gitignore
vendored
Normal file
1
logger/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
logger-bin
|
||||||
3
logger/go.mod
Normal file
3
logger/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module philologue.net/diachron/logger-bin
|
||||||
|
|
||||||
|
go 1.23.3
|
||||||
70
logger/main.go
Normal file
70
logger/main.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
port := flag.Int("port", 8085, "port to listen on")
|
||||||
|
capacity := flag.Int("capacity", 1000000, "max messages to store")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
store := NewLogStore(*capacity)
|
||||||
|
|
||||||
|
http.HandleFunc("POST /log", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var msg Message
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||||
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Add(msg)
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("GET /logs", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
params := FilterParams{}
|
||||||
|
|
||||||
|
if limit := r.URL.Query().Get("limit"); limit != "" {
|
||||||
|
if n, err := strconv.Atoi(limit); err == nil {
|
||||||
|
params.Limit = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if before := r.URL.Query().Get("before"); before != "" {
|
||||||
|
if ts, err := strconv.ParseInt(before, 10, 64); err == nil {
|
||||||
|
params.Before = ts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if after := r.URL.Query().Get("after"); after != "" {
|
||||||
|
if ts, err := strconv.ParseInt(after, 10, 64); err == nil {
|
||||||
|
params.After = ts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := store.GetFiltered(params)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(messages)
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("GET /status", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
status := map[string]any{
|
||||||
|
"count": store.Count(),
|
||||||
|
"capacity": *capacity,
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(status)
|
||||||
|
})
|
||||||
|
|
||||||
|
listenAddr := fmt.Sprintf(":%d", *port)
|
||||||
|
log.Printf("[logger] Listening on %s (capacity: %d)", listenAddr, *capacity)
|
||||||
|
if err := http.ListenAndServe(listenAddr, nil); err != nil {
|
||||||
|
log.Fatalf("[logger] Failed to start: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
126
logger/store.go
Normal file
126
logger/store.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message represents a log entry from the express backend
|
||||||
|
type Message struct {
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
Source string `json:"source"` // "logging" | "diagnostic" | "user"
|
||||||
|
Text []string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogStore is a thread-safe ring buffer for log messages
|
||||||
|
type LogStore struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
messages []Message
|
||||||
|
head int // next write position
|
||||||
|
full bool // whether buffer has wrapped
|
||||||
|
capacity int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogStore creates a new log store with the given capacity
|
||||||
|
func NewLogStore(capacity int) *LogStore {
|
||||||
|
return &LogStore{
|
||||||
|
messages: make([]Message, capacity),
|
||||||
|
capacity: capacity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add inserts a new message into the store
|
||||||
|
func (s *LogStore) Add(msg Message) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
s.messages[s.head] = msg
|
||||||
|
s.head++
|
||||||
|
if s.head >= s.capacity {
|
||||||
|
s.head = 0
|
||||||
|
s.full = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count returns the number of messages in the store
|
||||||
|
func (s *LogStore) Count() int {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
if s.full {
|
||||||
|
return s.capacity
|
||||||
|
}
|
||||||
|
return s.head
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRecent returns the most recent n messages, newest first
|
||||||
|
func (s *LogStore) GetRecent(n int) []Message {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
count := s.Count()
|
||||||
|
if n > count {
|
||||||
|
n = count
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Message, n)
|
||||||
|
pos := s.head - 1
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if pos < 0 {
|
||||||
|
pos = s.capacity - 1
|
||||||
|
}
|
||||||
|
result[i] = s.messages[pos]
|
||||||
|
pos--
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter parameters for retrieving logs
|
||||||
|
type FilterParams struct {
|
||||||
|
Limit int // max messages to return (0 = default 100)
|
||||||
|
Before int64 // only messages before this timestamp
|
||||||
|
After int64 // only messages after this timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFiltered returns messages matching the filter criteria
|
||||||
|
func (s *LogStore) GetFiltered(params FilterParams) []Message {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
limit := params.Limit
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
count := s.Count()
|
||||||
|
if count == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Message, 0, limit)
|
||||||
|
pos := s.head - 1
|
||||||
|
|
||||||
|
for i := 0; i < count && len(result) < limit; i++ {
|
||||||
|
if pos < 0 {
|
||||||
|
pos = s.capacity - 1
|
||||||
|
}
|
||||||
|
msg := s.messages[pos]
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if params.Before > 0 && msg.Timestamp >= params.Before {
|
||||||
|
pos--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if params.After > 0 && msg.Timestamp <= params.After {
|
||||||
|
pos--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, msg)
|
||||||
|
pos--
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user