mirror of
https://github.com/langgenius/dify-plugin-daemon.git
synced 2026-07-22 01:35:24 -04:00
ca3d00229e
* use slog instead of log package and format to new log schema * update the environment name to LOG_OUTPUT_FORMAT * add the env to .env.example * fix log reference error * change the order of milldlewares * delete unused code * fix the concurrently session potential race condition * fix the log format in tests * update the duplicate code * refactor: convert log functions to slog structured format - Change log.Error/Info/Warn/Debug/Panic to accept msg + key-value pairs - Remove printf-style formatting from log functions - Update log calls in internal/cluster, internal/db, internal/core/session_manager - Remove unused 'initialized' variable from log package - Remaining files will be updated in follow-up commits 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: update all log call sites to use slog structured format Convert all log.Error, log.Info, log.Warn, log.Debug, and log.Panic calls from printf-style formatting to slog key-value pairs. Before: log.Error("failed to do something: %s", err.Error()) After: log.Error("failed to do something", "error", err) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: update cmd/ log calls to use slog structured format 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: implement GnetLogger for structured logging in gnet * refactor: remove deprecated log visibility functions and related calls * feat: enhance session management with trace and identity context propagation * feat: implement serverless transaction handler and writer for plugin runtime * refactor: rename context field to traceCtx in RealBackwardsInvocation --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Yeuoly <admin@srmxy.cn>
297 lines
5.7 KiB
Go
297 lines
5.7 KiB
Go
package log
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Options struct {
|
|
Level slog.Leveler
|
|
Service string
|
|
JSON bool
|
|
Out io.Writer
|
|
}
|
|
|
|
type Handler struct {
|
|
opts Options
|
|
mu *sync.Mutex
|
|
attrs []slog.Attr
|
|
groups []string
|
|
}
|
|
|
|
func NewHandler(opts Options) *Handler {
|
|
if opts.Level == nil {
|
|
opts.Level = slog.LevelInfo
|
|
}
|
|
return &Handler{
|
|
opts: opts,
|
|
mu: &sync.Mutex{},
|
|
}
|
|
}
|
|
|
|
func (h *Handler) Enabled(_ context.Context, level slog.Level) bool {
|
|
return level >= h.opts.Level.Level()
|
|
}
|
|
|
|
func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
|
|
fields := make(map[string]any)
|
|
|
|
fields["ts"] = r.Time.UTC().Format(time.RFC3339Nano)
|
|
fields["severity"] = levelToSeverity(r.Level)
|
|
fields["service"] = h.opts.Service
|
|
fields["caller"] = getCaller(r.PC)
|
|
fields["message"] = r.Message
|
|
|
|
if tc, ok := TraceFromContext(ctx); ok && tc.TraceID != "" {
|
|
fields["trace_id"] = tc.TraceID
|
|
if tc.SpanID != "" {
|
|
fields["span_id"] = tc.SpanID
|
|
}
|
|
}
|
|
|
|
if id, ok := IdentityFromContext(ctx); ok {
|
|
identity := make(map[string]any)
|
|
if id.TenantID != "" {
|
|
identity["tenant_id"] = id.TenantID
|
|
}
|
|
if id.UserID != "" {
|
|
identity["user_id"] = id.UserID
|
|
}
|
|
if id.UserType != "" {
|
|
identity["user_type"] = id.UserType
|
|
}
|
|
if len(identity) > 0 {
|
|
fields["identity"] = identity
|
|
}
|
|
}
|
|
|
|
attrs := make(map[string]any)
|
|
for _, attr := range h.attrs {
|
|
collectAttr(attrs, attr)
|
|
}
|
|
r.Attrs(func(a slog.Attr) bool {
|
|
collectAttr(attrs, a)
|
|
return true
|
|
})
|
|
|
|
var stackTrace string
|
|
if st, ok := attrs["stack_trace"]; ok {
|
|
if s, isStr := st.(string); isStr {
|
|
stackTrace = s
|
|
}
|
|
delete(attrs, "stack_trace")
|
|
}
|
|
|
|
if errVal, ok := attrs["error"]; ok {
|
|
if err, isErr := errVal.(error); isErr {
|
|
attrs["error"] = err.Error()
|
|
if r.Level >= slog.LevelError && stackTrace == "" {
|
|
stackTrace = captureStackTrace()
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(attrs) > 0 {
|
|
fields["attributes"] = attrs
|
|
}
|
|
|
|
if stackTrace != "" {
|
|
fields["stack_trace"] = stackTrace
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if h.opts.JSON {
|
|
return h.writeJSON(fields)
|
|
}
|
|
return h.writeText(fields)
|
|
}
|
|
|
|
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
newAttrs := make([]slog.Attr, len(h.attrs)+len(attrs))
|
|
copy(newAttrs, h.attrs)
|
|
copy(newAttrs[len(h.attrs):], attrs)
|
|
return &Handler{
|
|
opts: h.opts,
|
|
mu: h.mu,
|
|
attrs: newAttrs,
|
|
groups: h.groups,
|
|
}
|
|
}
|
|
|
|
func (h *Handler) WithGroup(name string) slog.Handler {
|
|
if name == "" {
|
|
return h
|
|
}
|
|
newGroups := make([]string, len(h.groups)+1)
|
|
copy(newGroups, h.groups)
|
|
newGroups[len(h.groups)] = name
|
|
return &Handler{
|
|
opts: h.opts,
|
|
mu: h.mu,
|
|
attrs: h.attrs,
|
|
groups: newGroups,
|
|
}
|
|
}
|
|
|
|
func (h *Handler) writeJSON(fields map[string]any) error {
|
|
data, err := json.Marshal(fields)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = h.opts.Out.Write(append(data, '\n'))
|
|
return err
|
|
}
|
|
|
|
func (h *Handler) writeText(fields map[string]any) error {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString(fields["ts"].(string))
|
|
sb.WriteByte(' ')
|
|
sb.WriteString(fields["severity"].(string))
|
|
sb.WriteByte(' ')
|
|
sb.WriteString(fields["service"].(string))
|
|
sb.WriteByte(' ')
|
|
sb.WriteString(fields["caller"].(string))
|
|
|
|
if traceID, ok := fields["trace_id"].(string); ok {
|
|
sb.WriteString(" trace_id=")
|
|
sb.WriteString(traceID)
|
|
}
|
|
|
|
if identity, ok := fields["identity"].(map[string]any); ok {
|
|
if tenantID, ok := identity["tenant_id"].(string); ok {
|
|
sb.WriteString(" tenant_id=")
|
|
sb.WriteString(tenantID)
|
|
}
|
|
if userID, ok := identity["user_id"].(string); ok {
|
|
sb.WriteString(" user_id=")
|
|
sb.WriteString(userID)
|
|
}
|
|
}
|
|
|
|
sb.WriteByte(' ')
|
|
sb.WriteString(fields["message"].(string))
|
|
|
|
if attrs, ok := fields["attributes"].(map[string]any); ok {
|
|
for k, v := range attrs {
|
|
sb.WriteByte(' ')
|
|
sb.WriteString(k)
|
|
sb.WriteByte('=')
|
|
sb.WriteString(formatValue(v))
|
|
}
|
|
}
|
|
|
|
sb.WriteByte('\n')
|
|
|
|
if stackTrace, ok := fields["stack_trace"].(string); ok && stackTrace != "" {
|
|
sb.WriteString(stackTrace)
|
|
sb.WriteByte('\n')
|
|
}
|
|
|
|
_, err := h.opts.Out.Write([]byte(sb.String()))
|
|
return err
|
|
}
|
|
|
|
func levelToSeverity(level slog.Level) string {
|
|
switch {
|
|
case level >= slog.LevelError:
|
|
return "ERROR"
|
|
case level >= slog.LevelWarn:
|
|
return "WARN"
|
|
case level >= slog.LevelInfo:
|
|
return "INFO"
|
|
default:
|
|
return "DEBUG"
|
|
}
|
|
}
|
|
|
|
func getCaller(pc uintptr) string {
|
|
if pc == 0 {
|
|
return "unknown"
|
|
}
|
|
fs := runtime.CallersFrames([]uintptr{pc})
|
|
f, _ := fs.Next()
|
|
if f.File == "" {
|
|
return "unknown"
|
|
}
|
|
idx := strings.LastIndex(f.File, "/")
|
|
if idx >= 0 {
|
|
f.File = f.File[idx+1:]
|
|
}
|
|
return f.File + ":" + strconv.Itoa(f.Line)
|
|
}
|
|
|
|
func collectAttr(attrs map[string]any, a slog.Attr) {
|
|
if a.Key == "" {
|
|
return
|
|
}
|
|
switch a.Value.Kind() {
|
|
case slog.KindGroup:
|
|
group := make(map[string]any)
|
|
for _, ga := range a.Value.Group() {
|
|
collectAttr(group, ga)
|
|
}
|
|
if len(group) > 0 {
|
|
attrs[a.Key] = group
|
|
}
|
|
default:
|
|
attrs[a.Key] = a.Value.Any()
|
|
}
|
|
}
|
|
|
|
func formatValue(v any) string {
|
|
switch val := v.(type) {
|
|
case string:
|
|
if strings.ContainsAny(val, " \t\n\"") {
|
|
return strconv.Quote(val)
|
|
}
|
|
return val
|
|
case error:
|
|
return strconv.Quote(val.Error())
|
|
default:
|
|
return fmt.Sprintf("%v", v)
|
|
}
|
|
}
|
|
|
|
func captureStackTrace() string {
|
|
const depth = 32
|
|
var pcs [depth]uintptr
|
|
n := runtime.Callers(5, pcs[:])
|
|
if n == 0 {
|
|
return ""
|
|
}
|
|
|
|
var sb strings.Builder
|
|
frames := runtime.CallersFrames(pcs[:n])
|
|
for {
|
|
frame, more := frames.Next()
|
|
if strings.Contains(frame.File, "runtime/") {
|
|
if !more {
|
|
break
|
|
}
|
|
continue
|
|
}
|
|
sb.WriteString(frame.Function)
|
|
sb.WriteByte('\n')
|
|
sb.WriteByte('\t')
|
|
sb.WriteString(frame.File)
|
|
sb.WriteByte(':')
|
|
sb.WriteString(strconv.Itoa(frame.Line))
|
|
sb.WriteByte('\n')
|
|
if !more {
|
|
break
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|