| package proxy |
|
|
| import ( |
| "bytes" |
| "encoding/json" |
| "io" |
| "log" |
| "os" |
| "path/filepath" |
| "strings" |
| "sync" |
| "sync/atomic" |
| ) |
|
|
| var debugLoggingEnabled atomic.Bool |
| var apiInputLoggingEnabled atomic.Bool |
| var apiOutputLoggingEnabled atomic.Bool |
| var notionRequestLoggingEnabled atomic.Bool |
| var notionResponseLoggingEnabled atomic.Bool |
| var globalLogWriter = newDebugFilterWriter() |
|
|
| var debugLogTags = []string{ |
| "[debug]", |
| "[bridge]", |
| "[session]", |
| "[thinking]", |
| "[req]", |
| "[search]", |
| "[upload-debug]", |
| } |
|
|
| type debugFilterWriter struct { |
| mu sync.RWMutex |
| out io.Writer |
| file *os.File |
| } |
|
|
| func newDebugFilterWriter() *debugFilterWriter { |
| return &debugFilterWriter{ |
| out: os.Stderr, |
| } |
| } |
|
|
| func init() { |
| debugLoggingEnabled.Store(true) |
| log.SetOutput(globalLogWriter) |
| } |
|
|
| func (w *debugFilterWriter) Write(p []byte) (int, error) { |
| if !DebugLoggingEnabled() && isDebugLogLine(p) { |
| return len(p), nil |
| } |
| w.mu.RLock() |
| out := w.out |
| w.mu.RUnlock() |
| if out == nil { |
| out = os.Stderr |
| } |
| return out.Write(p) |
| } |
|
|
| func (w *debugFilterWriter) SetOutputPath(path string) error { |
| path = strings.TrimSpace(path) |
|
|
| target := io.Writer(os.Stderr) |
| var file *os.File |
|
|
| if path != "" { |
| dir := filepath.Dir(path) |
| if dir != "." && dir != "" { |
| if err := os.MkdirAll(dir, 0755); err != nil { |
| return err |
| } |
| } |
|
|
| f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) |
| if err != nil { |
| return err |
| } |
| target = f |
| file = f |
| } |
|
|
| w.mu.Lock() |
| oldFile := w.file |
| w.out = target |
| w.file = file |
| w.mu.Unlock() |
|
|
| if oldFile != nil && oldFile != file { |
| _ = oldFile.Close() |
| } |
| return nil |
| } |
|
|
| func isDebugLogLine(p []byte) bool { |
| line := string(p) |
| for _, tag := range debugLogTags { |
| if strings.Contains(line, tag) { |
| return true |
| } |
| } |
| return false |
| } |
|
|
| func SetDebugLoggingEnabled(enabled bool) { |
| debugLoggingEnabled.Store(enabled) |
| } |
|
|
| func DebugLoggingEnabled() bool { |
| return debugLoggingEnabled.Load() |
| } |
|
|
| func ConfigureLogOutput(path string) error { |
| return globalLogWriter.SetOutputPath(path) |
| } |
|
|
| func SetAPILogInputEnabled(enabled bool) { |
| apiInputLoggingEnabled.Store(enabled) |
| } |
|
|
| func APILogInputEnabled() bool { |
| return apiInputLoggingEnabled.Load() |
| } |
|
|
| func SetAPILogOutputEnabled(enabled bool) { |
| apiOutputLoggingEnabled.Store(enabled) |
| } |
|
|
| func APILogOutputEnabled() bool { |
| return apiOutputLoggingEnabled.Load() |
| } |
|
|
| func SetNotionRequestLoggingEnabled(enabled bool) { |
| notionRequestLoggingEnabled.Store(enabled) |
| } |
|
|
| func NotionRequestLoggingEnabled() bool { |
| return notionRequestLoggingEnabled.Load() |
| } |
|
|
| func SetNotionResponseLoggingEnabled(enabled bool) { |
| notionResponseLoggingEnabled.Store(enabled) |
| } |
|
|
| func NotionResponseLoggingEnabled() bool { |
| return notionResponseLoggingEnabled.Load() |
| } |
|
|
| func LogAPIInputJSON(requestID, label string, v interface{}) { |
| if !APILogInputEnabled() { |
| return |
| } |
| logJSONPayload("[api-in]", requestID, label, v) |
| } |
|
|
| func LogAPIInputJSONBytes(requestID, label string, raw []byte) { |
| if !APILogInputEnabled() { |
| return |
| } |
| logJSONBytesPayload("[api-in]", requestID, label, raw) |
| } |
|
|
| func LogAPIInputText(requestID, label, text string) { |
| if !APILogInputEnabled() { |
| return |
| } |
| logTextPayload("[api-in]", requestID, label, text) |
| } |
|
|
| func LogAPIOutputJSON(requestID, label string, v interface{}) { |
| if !APILogOutputEnabled() { |
| return |
| } |
| logJSONPayload("[api-out]", requestID, label, v) |
| } |
|
|
| func LogAPIOutputText(requestID, label, text string) { |
| if !APILogOutputEnabled() { |
| return |
| } |
| logTextPayload("[api-out]", requestID, label, text) |
| } |
|
|
| func LogNotionRequestJSON(requestID, label string, v interface{}) { |
| if !NotionRequestLoggingEnabled() { |
| return |
| } |
| logJSONPayload("[notion-req]", requestID, label, v) |
| } |
|
|
| func LogNotionResponseJSON(requestID, label string, v interface{}) { |
| if !NotionResponseLoggingEnabled() { |
| return |
| } |
| logJSONPayload("[notion-resp]", requestID, label, v) |
| } |
|
|
| func LogNotionResponseJSONBytes(requestID, label string, raw []byte) { |
| if !NotionResponseLoggingEnabled() { |
| return |
| } |
| logJSONBytesPayload("[notion-resp]", requestID, label, raw) |
| } |
|
|
| func LogNotionResponseText(requestID, label, text string) { |
| if !NotionResponseLoggingEnabled() { |
| return |
| } |
| logTextPayload("[notion-resp]", requestID, label, text) |
| } |
|
|
| func logJSONPayload(tag, requestID, label string, v interface{}) { |
| raw, err := json.MarshalIndent(v, "", " ") |
| if err != nil { |
| log.Printf("%s %s %s marshal error: %v", tag, requestID, label, err) |
| return |
| } |
| logTextPayload(tag, requestID, label, string(raw)) |
| } |
|
|
| func logJSONBytesPayload(tag, requestID, label string, raw []byte) { |
| var buf bytes.Buffer |
| if err := json.Indent(&buf, raw, "", " "); err == nil { |
| logTextPayload(tag, requestID, label, buf.String()) |
| return |
| } |
| logTextPayload(tag, requestID, label, string(raw)) |
| } |
|
|
| func logTextPayload(tag, requestID, label, text string) { |
| label = strings.TrimSpace(label) |
| requestID = strings.TrimSpace(requestID) |
| if requestID != "" && label != "" { |
| log.Printf("%s %s %s\n%s", tag, requestID, label, text) |
| return |
| } |
| if requestID != "" { |
| log.Printf("%s %s\n%s", tag, requestID, text) |
| return |
| } |
| if label != "" { |
| log.Printf("%s %s\n%s", tag, label, text) |
| return |
| } |
| log.Printf("%s\n%s", tag, text) |
| } |
|
|