- Go service bridging deepseek.de/es/fr V4 models behind an OpenAI-compatible API - /v1/chat/completions, /v1/responses, /v1/messages (Anthropic) with tool calling - web admin console: dashboard, models/sites, keys, proxies, Turnstile, playground, live logs - Turnstile solvers: solver API / local browser via CDP / manual cookie import - MIT license, README with screenshots, Dockerfile and Makefile
239 lines
4.6 KiB
Go
239 lines
4.6 KiB
Go
// Package logbuf keeps a ring buffer of log entries and fans them out to
|
|
// live subscribers for the web console.
|
|
package logbuf
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Entry struct {
|
|
TS time.Time `json:"ts"`
|
|
Level string `json:"level"`
|
|
Msg string `json:"msg"`
|
|
Attrs map[string]string `json:"attrs,omitempty"`
|
|
}
|
|
|
|
type Buffer struct {
|
|
mu sync.Mutex
|
|
entries []Entry
|
|
subs map[int]chan Entry
|
|
nextID int
|
|
limit int
|
|
}
|
|
|
|
func New(limit int) *Buffer {
|
|
if limit <= 0 {
|
|
limit = 500
|
|
}
|
|
return &Buffer{subs: map[int]chan Entry{}, limit: limit}
|
|
}
|
|
|
|
func (b *Buffer) Add(e Entry) {
|
|
b.mu.Lock()
|
|
b.entries = append(b.entries, e)
|
|
if len(b.entries) > b.limit {
|
|
b.entries = b.entries[len(b.entries)-b.limit:]
|
|
}
|
|
chans := make([]chan Entry, 0, len(b.subs))
|
|
for _, ch := range b.subs {
|
|
chans = append(chans, ch)
|
|
}
|
|
b.mu.Unlock()
|
|
|
|
for _, ch := range chans {
|
|
select {
|
|
case ch <- e:
|
|
default:
|
|
// slow consumer: drop rather than block the logger
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recent returns up to n entries, optionally filtered by minimum level.
|
|
func (b *Buffer) Recent(minLevel string, n int) []Entry {
|
|
min := levelValue(minLevel)
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
out := make([]Entry, 0, n)
|
|
for i := len(b.entries) - 1; i >= 0 && len(out) < n; i-- {
|
|
if levelValue(b.entries[i].Level) >= min {
|
|
out = append(out, b.entries[i])
|
|
}
|
|
}
|
|
// reverse to chronological order
|
|
for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
|
|
out[i], out[j] = out[j], out[i]
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Subscribe returns a channel of new entries plus an unsubscribe func.
|
|
func (b *Buffer) Subscribe() (<-chan Entry, func()) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
id := b.nextID
|
|
b.nextID++
|
|
ch := make(chan Entry, 128)
|
|
b.subs[id] = ch
|
|
return ch, func() {
|
|
b.mu.Lock()
|
|
if c, ok := b.subs[id]; ok {
|
|
delete(b.subs, id)
|
|
close(c)
|
|
}
|
|
b.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
func levelValue(l string) int {
|
|
switch l {
|
|
case "DEBUG", "debug":
|
|
return -4
|
|
case "INFO", "info":
|
|
return 0
|
|
case "WARN", "WARNING", "warn", "warning":
|
|
return 4
|
|
case "ERROR", "error":
|
|
return 8
|
|
default:
|
|
return -100
|
|
}
|
|
}
|
|
|
|
// Handler is an slog.Handler that mirrors records into the buffer.
|
|
type Handler struct {
|
|
parent slog.Handler
|
|
buf *Buffer
|
|
attrs []slog.Attr
|
|
group string
|
|
}
|
|
|
|
func NewHandler(parent slog.Handler, buf *Buffer) *Handler {
|
|
return &Handler{parent: parent, buf: buf}
|
|
}
|
|
|
|
func (h *Handler) Enabled(_ context.Context, l slog.Level) bool {
|
|
return h.parent.Enabled(context.Background(), l)
|
|
}
|
|
|
|
func (h *Handler) Handle(_ context.Context, r slog.Record) error {
|
|
attrs := map[string]string{}
|
|
for _, a := range h.attrs {
|
|
attrs[a.Key] = fmtValue(a.Value)
|
|
}
|
|
r.Attrs(func(a slog.Attr) bool {
|
|
attrs[a.Key] = fmtValue(a.Value)
|
|
return true
|
|
})
|
|
if h.group != "" {
|
|
grouped := map[string]string{}
|
|
for k, v := range attrs {
|
|
grouped[h.group+"."+k] = v
|
|
}
|
|
attrs = grouped
|
|
}
|
|
level := "INFO"
|
|
switch {
|
|
case r.Level >= slog.LevelError:
|
|
level = "ERROR"
|
|
case r.Level >= slog.LevelWarn:
|
|
level = "WARN"
|
|
case r.Level >= slog.LevelInfo:
|
|
level = "INFO"
|
|
default:
|
|
level = "DEBUG"
|
|
}
|
|
h.buf.Add(Entry{TS: r.Time, Level: level, Msg: r.Message, Attrs: attrs})
|
|
return h.parent.Handle(context.Background(), r)
|
|
}
|
|
|
|
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
cp := *h
|
|
cp.attrs = append(append([]slog.Attr{}, h.attrs...), attrs...)
|
|
return &cp
|
|
}
|
|
|
|
func (h *Handler) WithGroup(name string) slog.Handler {
|
|
cp := *h
|
|
if h.group == "" {
|
|
cp.group = name
|
|
} else {
|
|
cp.group = h.group + "." + name
|
|
}
|
|
return &cp
|
|
}
|
|
|
|
func fmtValue(v slog.Value) string {
|
|
switch v.Kind() {
|
|
case slog.KindString:
|
|
return v.String()
|
|
case slog.KindInt64:
|
|
return itoa(v.Int64())
|
|
case slog.KindUint64:
|
|
return utoa(v.Uint64())
|
|
case slog.KindFloat64:
|
|
return ftoa(v.Float64())
|
|
case slog.KindBool:
|
|
if v.Bool() {
|
|
return "true"
|
|
}
|
|
return "false"
|
|
case slog.KindTime:
|
|
return v.Time().Format(time.RFC3339)
|
|
case slog.KindDuration:
|
|
return v.Duration().String()
|
|
case slog.KindAny:
|
|
return anyString(v.Any())
|
|
default:
|
|
return v.String()
|
|
}
|
|
}
|
|
|
|
func anyString(v any) string {
|
|
switch t := v.(type) {
|
|
case error:
|
|
if t == nil {
|
|
return ""
|
|
}
|
|
return t.Error()
|
|
case string:
|
|
return t
|
|
case nil:
|
|
return ""
|
|
default:
|
|
return fmt.Sprintf("%v", t)
|
|
}
|
|
}
|
|
|
|
func itoa(v int64) string {
|
|
if v == 0 {
|
|
return "0"
|
|
}
|
|
neg := v < 0
|
|
if neg {
|
|
v = -v
|
|
}
|
|
var buf [24]byte
|
|
i := len(buf)
|
|
for v > 0 {
|
|
i--
|
|
buf[i] = byte('0' + v%10)
|
|
v /= 10
|
|
}
|
|
if neg {
|
|
i--
|
|
buf[i] = '-'
|
|
}
|
|
return string(buf[i:])
|
|
}
|
|
|
|
func utoa(v uint64) string { return itoa(int64(v)) }
|
|
|
|
func ftoa(f float64) string {
|
|
return fmt.Sprintf("%g", f)
|
|
}
|