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)
|
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 != "" {
|
if model != "" {
|
||||||
cfg.Agents.Defaults.ModelName = model
|
cfg.Agents.Defaults.ModelName = model
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,11 @@ func gatewayCmd(debug bool) error {
|
||||||
return fmt.Errorf("error loading config: %w", err)
|
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)
|
provider, modelID, err := providers.CreateProvider(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating provider: %w", err)
|
return fmt.Errorf("error creating provider: %w", err)
|
||||||
|
|
@ -198,8 +203,6 @@ func gatewayCmd(debug bool) error {
|
||||||
// since the original ctx is already canceled.
|
// since the original ctx is already canceled.
|
||||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second)
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer shutdownCancel()
|
defer shutdownCancel()
|
||||||
|
|
||||||
channelManager.StopAll(shutdownCtx)
|
|
||||||
deviceService.Stop()
|
deviceService.Stop()
|
||||||
heartbeatService.Stop()
|
heartbeatService.Stop()
|
||||||
cronService.Stop()
|
cronService.Stop()
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@ func statusCmd() {
|
||||||
return
|
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()
|
configPath := internal.GetConfigPath()
|
||||||
|
|
||||||
fmt.Printf("%s picoclaw Status\n", internal.Logo)
|
fmt.Printf("%s picoclaw Status\n", internal.Logo)
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,14 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"log"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/caarlos0/env/v11"
|
"github.com/caarlos0/env/v11"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/fileutil"
|
"github.com/sipeed/picoclaw/pkg/fileutil"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// rrCounter is a global counter for round-robin load balancing across models.
|
// rrCounter is a global counter for round-robin load balancing across models.
|
||||||
|
|
@ -594,6 +597,8 @@ type ToolsConfig struct {
|
||||||
Skills SkillsToolsConfig `json:"skills"`
|
Skills SkillsToolsConfig `json:"skills"`
|
||||||
MediaCleanup MediaCleanupConfig `json:"media_cleanup"`
|
MediaCleanup MediaCleanupConfig `json:"media_cleanup"`
|
||||||
MCP MCPConfig `json:"mcp"`
|
MCP MCPConfig `json:"mcp"`
|
||||||
|
Logging LoggingConfig `json:"logging"`
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SkillsToolsConfig struct {
|
type SkillsToolsConfig struct {
|
||||||
|
|
@ -651,6 +656,16 @@ type MCPConfig struct {
|
||||||
Servers map[string]MCPServerConfig `json:"servers,omitempty"`
|
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) {
|
func LoadConfig(path string) (*Config, error) {
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
|
|
||||||
|
|
@ -832,3 +847,49 @@ func (c *Config) ValidateModelList() error {
|
||||||
}
|
}
|
||||||
return nil
|
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
|
FATAL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type LogFormat string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TextFormat LogFormat = "text"
|
||||||
|
JSONFormat LogFormat = "json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logFormat = JSONFormat // Default format
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logLevelNames = map[LogLevel]string{
|
logLevelNames = map[LogLevel]string{
|
||||||
DEBUG: "DEBUG",
|
DEBUG: "DEBUG",
|
||||||
|
|
@ -66,6 +77,18 @@ func GetLevel() LogLevel {
|
||||||
defer mu.RUnlock()
|
defer mu.RUnlock()
|
||||||
return currentLevel
|
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 {
|
func EnableFileLogging(filePath string) error {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
|
|
@ -116,35 +139,24 @@ func logMessage(level LogLevel, component string, message string, fields map[str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
outputData := formatLogEntry(entry)
|
||||||
if logger.file != nil {
|
if logger.file != nil {
|
||||||
jsonData, err := json.Marshal(entry)
|
logger.file.Write(outputData)
|
||||||
if err == nil {
|
|
||||||
logger.file.Write(append(jsonData, '\n'))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var fieldStr string
|
// For console/terminal output, use the same format as configured
|
||||||
if len(fields) > 0 {
|
consoleOutput := formatLogForConsole(entry)
|
||||||
fieldStr = " " + formatFields(fields)
|
if len(consoleOutput) > 0 {
|
||||||
} else {
|
logLine := string(consoleOutput[:len(consoleOutput)-1]) // Remove trailing newline
|
||||||
fieldStr = ""
|
log.Println(logLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
logLine := fmt.Sprintf("[%s] [%s]%s %s%s",
|
|
||||||
entry.Timestamp,
|
|
||||||
logLevelNames[level],
|
|
||||||
formatComponent(component),
|
|
||||||
message,
|
|
||||||
fieldStr,
|
|
||||||
)
|
|
||||||
|
|
||||||
log.Println(logLine)
|
|
||||||
|
|
||||||
if level == FATAL {
|
if level == FATAL {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func formatComponent(component string) string {
|
func formatComponent(component string) string {
|
||||||
if component == "" {
|
if component == "" {
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -160,10 +172,75 @@ func formatFields(fields map[string]any) string {
|
||||||
return fmt.Sprintf("{%s}", strings.Join(parts, ", "))
|
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) {
|
func Debug(message string) {
|
||||||
logMessage(DEBUG, "", message, nil)
|
logMessage(DEBUG, "", message, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func DebugC(component string, message string) {
|
func DebugC(component string, message string) {
|
||||||
logMessage(DEBUG, component, message, nil)
|
logMessage(DEBUG, component, message, nil)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue