feat: Add structured logging support with configurable format and level
This commit is contained in:
parent
ebe425a86f
commit
6039e1ed93
5 changed files with 172 additions and 21 deletions
|
|
@ -33,6 +33,11 @@ func agentCmd(message, sessionKey, model string, debug bool) error {
|
|||
return fmt.Errorf("error loading config: %w", err)
|
||||
}
|
||||
|
||||
// Apply logging configuration from config file
|
||||
if err := cfg.ApplyLoggingConfig(); err != nil {
|
||||
fmt.Printf("Warning: Failed to apply logging config: %v\n", err)
|
||||
}
|
||||
|
||||
if model != "" {
|
||||
cfg.Agents.Defaults.ModelName = model
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,11 @@ func gatewayCmd(debug bool) error {
|
|||
return fmt.Errorf("error loading config: %w", err)
|
||||
}
|
||||
|
||||
// Apply logging configuration from config file
|
||||
if err := cfg.ApplyLoggingConfig(); err != nil {
|
||||
fmt.Printf("Warning: Failed to apply logging config: %v\n", err)
|
||||
}
|
||||
|
||||
provider, modelID, err := providers.CreateProvider(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating provider: %w", err)
|
||||
|
|
@ -198,8 +203,6 @@ func gatewayCmd(debug bool) error {
|
|||
// since the original ctx is already canceled.
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer shutdownCancel()
|
||||
|
||||
channelManager.StopAll(shutdownCtx)
|
||||
deviceService.Stop()
|
||||
heartbeatService.Stop()
|
||||
cronService.Stop()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ func statusCmd() {
|
|||
return
|
||||
}
|
||||
|
||||
// Apply logging configuration from config file
|
||||
if err := cfg.ApplyLoggingConfig(); err != nil {
|
||||
fmt.Printf("Warning: Failed to apply logging config: %v\n", err)
|
||||
}
|
||||
|
||||
configPath := internal.GetConfigPath()
|
||||
|
||||
fmt.Printf("%s picoclaw Status\n", internal.Logo)
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"log"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/caarlos0/env/v11"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/fileutil"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
)
|
||||
|
||||
// rrCounter is a global counter for round-robin load balancing across models.
|
||||
|
|
@ -594,6 +597,8 @@ type ToolsConfig struct {
|
|||
Skills SkillsToolsConfig `json:"skills"`
|
||||
MediaCleanup MediaCleanupConfig `json:"media_cleanup"`
|
||||
MCP MCPConfig `json:"mcp"`
|
||||
Logging LoggingConfig `json:"logging"`
|
||||
|
||||
}
|
||||
|
||||
type SkillsToolsConfig struct {
|
||||
|
|
@ -651,6 +656,16 @@ type MCPConfig struct {
|
|||
Servers map[string]MCPServerConfig `json:"servers,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
|
||||
// LoggingConfig holds the logging configuration.
|
||||
type LoggingConfig struct {
|
||||
Level string `json:"level" env:"PICOCLAW_LOGGING_LEVEL"`
|
||||
Format string `json:"format" env:"PICOCLAW_LOGGING_FORMAT"`
|
||||
Output string `json:"output" env:"PICOCLAW_LOGGING_OUTPUT"`
|
||||
FilePath string `json:"file_path" env:"PICOCLAW_LOGGING_FILE_PATH"`
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
|
|
@ -832,3 +847,49 @@ func (c *Config) ValidateModelList() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// ApplyLoggingConfig applies the logging configuration from the tools section
|
||||
func (c *Config) ApplyLoggingConfig() error {
|
||||
// Set log level from config
|
||||
if c.Tools.Logging.Level != "" {
|
||||
switch strings.ToLower(c.Tools.Logging.Level) {
|
||||
case "debug":
|
||||
logger.SetLevel(logger.DEBUG)
|
||||
case "info":
|
||||
logger.SetLevel(logger.INFO)
|
||||
case "warn":
|
||||
logger.SetLevel(logger.WARN)
|
||||
case "error":
|
||||
logger.SetLevel(logger.ERROR)
|
||||
case "fatal":
|
||||
logger.SetLevel(logger.FATAL)
|
||||
default:
|
||||
logger.SetLevel(logger.INFO) // default
|
||||
}
|
||||
}
|
||||
|
||||
// Set log format from config
|
||||
if c.Tools.Logging.Format != "" {
|
||||
switch strings.ToLower(c.Tools.Logging.Format) {
|
||||
case "json":
|
||||
logger.SetFormat(logger.JSONFormat)
|
||||
case "text", "human":
|
||||
logger.SetFormat(logger.TextFormat)
|
||||
default:
|
||||
logger.SetFormat(logger.JSONFormat) // default
|
||||
}
|
||||
}
|
||||
|
||||
// Set file logging from config
|
||||
if c.Tools.Logging.Output == "file" || c.Tools.Logging.Output == "both" {
|
||||
if c.Tools.Logging.FilePath != "" {
|
||||
if err := logger.EnableFileLogging(c.Tools.Logging.FilePath); err != nil {
|
||||
log.Printf("Failed to enable file logging to %s: %v", c.Tools.Logging.FilePath, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,17 @@ const (
|
|||
FATAL
|
||||
)
|
||||
|
||||
type LogFormat string
|
||||
|
||||
const (
|
||||
TextFormat LogFormat = "text"
|
||||
JSONFormat LogFormat = "json"
|
||||
)
|
||||
|
||||
var (
|
||||
logFormat = JSONFormat // Default format
|
||||
)
|
||||
|
||||
var (
|
||||
logLevelNames = map[LogLevel]string{
|
||||
DEBUG: "DEBUG",
|
||||
|
|
@ -66,6 +77,18 @@ func GetLevel() LogLevel {
|
|||
defer mu.RUnlock()
|
||||
return currentLevel
|
||||
}
|
||||
func SetFormat(format LogFormat) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
logFormat = format
|
||||
}
|
||||
|
||||
func GetFormat() LogFormat {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return logFormat
|
||||
}
|
||||
|
||||
|
||||
func EnableFileLogging(filePath string) error {
|
||||
mu.Lock()
|
||||
|
|
@ -116,35 +139,24 @@ func logMessage(level LogLevel, component string, message string, fields map[str
|
|||
}
|
||||
}
|
||||
|
||||
outputData := formatLogEntry(entry)
|
||||
if logger.file != nil {
|
||||
jsonData, err := json.Marshal(entry)
|
||||
if err == nil {
|
||||
logger.file.Write(append(jsonData, '\n'))
|
||||
}
|
||||
logger.file.Write(outputData)
|
||||
}
|
||||
|
||||
var fieldStr string
|
||||
if len(fields) > 0 {
|
||||
fieldStr = " " + formatFields(fields)
|
||||
} else {
|
||||
fieldStr = ""
|
||||
}
|
||||
|
||||
logLine := fmt.Sprintf("[%s] [%s]%s %s%s",
|
||||
entry.Timestamp,
|
||||
logLevelNames[level],
|
||||
formatComponent(component),
|
||||
message,
|
||||
fieldStr,
|
||||
)
|
||||
|
||||
// For console/terminal output, use the same format as configured
|
||||
consoleOutput := formatLogForConsole(entry)
|
||||
if len(consoleOutput) > 0 {
|
||||
logLine := string(consoleOutput[:len(consoleOutput)-1]) // Remove trailing newline
|
||||
log.Println(logLine)
|
||||
}
|
||||
|
||||
if level == FATAL {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func formatComponent(component string) string {
|
||||
if component == "" {
|
||||
return ""
|
||||
|
|
@ -160,10 +172,75 @@ func formatFields(fields map[string]any) string {
|
|||
return fmt.Sprintf("{%s}", strings.Join(parts, ", "))
|
||||
}
|
||||
|
||||
func formatLogEntryText(entry LogEntry) []byte {
|
||||
var fieldStr string
|
||||
if len(entry.Fields) > 0 {
|
||||
fieldStr = " " + formatFields(entry.Fields)
|
||||
} else {
|
||||
fieldStr = ""
|
||||
}
|
||||
|
||||
logLine := fmt.Sprintf("[%s] [%s]%s %s%s",
|
||||
entry.Timestamp,
|
||||
entry.Level,
|
||||
formatComponent(entry.Component),
|
||||
entry.Message,
|
||||
fieldStr,
|
||||
)
|
||||
return []byte(fmt.Sprintf("%s\n", logLine))
|
||||
}
|
||||
|
||||
|
||||
func formatLogEntry(entry LogEntry) []byte {
|
||||
switch logFormat {
|
||||
case JSONFormat:
|
||||
jsonData, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
// Fallback to text format if JSON marshal fails
|
||||
return []byte(fmt.Sprintf("%s\n", entry.Message))
|
||||
}
|
||||
return append(jsonData, '\n')
|
||||
default:
|
||||
// Text format
|
||||
var fieldStr string
|
||||
if len(entry.Fields) > 0 {
|
||||
fieldStr = " " + formatFields(entry.Fields)
|
||||
} else {
|
||||
fieldStr = ""
|
||||
}
|
||||
|
||||
logLine := fmt.Sprintf("[%s] [%s]%s %s%s",
|
||||
entry.Timestamp,
|
||||
entry.Level,
|
||||
formatComponent(entry.Component),
|
||||
entry.Message,
|
||||
fieldStr,
|
||||
)
|
||||
return []byte(fmt.Sprintf("%s\n", logLine))
|
||||
}
|
||||
}
|
||||
|
||||
func formatLogForConsole(entry LogEntry) []byte {
|
||||
switch logFormat {
|
||||
case JSONFormat:
|
||||
jsonData, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
return []byte(fmt.Sprintf("%s\n", entry.Message))
|
||||
}
|
||||
return append(jsonData, '\n')
|
||||
default:
|
||||
// Use text format for console output
|
||||
return formatLogEntryText(entry)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func Debug(message string) {
|
||||
logMessage(DEBUG, "", message, nil)
|
||||
}
|
||||
|
||||
|
||||
func DebugC(component string, message string) {
|
||||
logMessage(DEBUG, component, message, nil)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue