v0.3.5 centralized formattig, refactored
This commit is contained in:
38
src/internal/format/format.go
Normal file
38
src/internal/format/format.go
Normal file
@ -0,0 +1,38 @@
|
||||
// FILE: src/internal/format/format.go
|
||||
package format
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"logwisp/src/internal/source"
|
||||
|
||||
"github.com/lixenwraith/log"
|
||||
)
|
||||
|
||||
// Formatter defines the interface for transforming a LogEntry into a byte slice.
|
||||
type Formatter interface {
|
||||
// Format takes a LogEntry and returns the formatted log as a byte slice.
|
||||
Format(entry source.LogEntry) ([]byte, error)
|
||||
|
||||
// Name returns the formatter type name
|
||||
Name() string
|
||||
}
|
||||
|
||||
// New creates a new Formatter based on the provided configuration.
|
||||
func New(name string, options map[string]any, logger *log.Logger) (Formatter, error) {
|
||||
// Default to raw if no format specified
|
||||
if name == "" {
|
||||
name = "raw"
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "json":
|
||||
return NewJSONFormatter(options, logger)
|
||||
case "text":
|
||||
return NewTextFormatter(options, logger)
|
||||
case "raw":
|
||||
return NewRawFormatter(options, logger)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown formatter type: %s", name)
|
||||
}
|
||||
}
|
||||
157
src/internal/format/json.go
Normal file
157
src/internal/format/json.go
Normal file
@ -0,0 +1,157 @@
|
||||
// FILE: src/internal/format/json.go
|
||||
package format
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"logwisp/src/internal/source"
|
||||
|
||||
"github.com/lixenwraith/log"
|
||||
)
|
||||
|
||||
// JSONFormatter produces structured JSON logs
|
||||
type JSONFormatter struct {
|
||||
pretty bool
|
||||
timestampField string
|
||||
levelField string
|
||||
messageField string
|
||||
sourceField string
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewJSONFormatter creates a new JSON formatter
|
||||
func NewJSONFormatter(options map[string]any, logger *log.Logger) (*JSONFormatter, error) {
|
||||
f := &JSONFormatter{
|
||||
timestampField: "timestamp",
|
||||
levelField: "level",
|
||||
messageField: "message",
|
||||
sourceField: "source",
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Extract options
|
||||
if pretty, ok := options["pretty"].(bool); ok {
|
||||
f.pretty = pretty
|
||||
}
|
||||
if field, ok := options["timestamp_field"].(string); ok && field != "" {
|
||||
f.timestampField = field
|
||||
}
|
||||
if field, ok := options["level_field"].(string); ok && field != "" {
|
||||
f.levelField = field
|
||||
}
|
||||
if field, ok := options["message_field"].(string); ok && field != "" {
|
||||
f.messageField = field
|
||||
}
|
||||
if field, ok := options["source_field"].(string); ok && field != "" {
|
||||
f.sourceField = field
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Format formats the log entry as JSON
|
||||
func (f *JSONFormatter) Format(entry source.LogEntry) ([]byte, error) {
|
||||
// Start with a clean map
|
||||
output := make(map[string]any)
|
||||
|
||||
// First, populate with LogWisp metadata
|
||||
output[f.timestampField] = entry.Time.Format(time.RFC3339Nano)
|
||||
output[f.levelField] = entry.Level
|
||||
output[f.sourceField] = entry.Source
|
||||
|
||||
// Try to parse the message as JSON
|
||||
var msgData map[string]any
|
||||
if err := json.Unmarshal([]byte(entry.Message), &msgData); err == nil {
|
||||
// Message is valid JSON - merge fields
|
||||
// LogWisp metadata takes precedence
|
||||
for k, v := range msgData {
|
||||
// Don't overwrite our standard fields
|
||||
if k != f.timestampField && k != f.levelField && k != f.sourceField {
|
||||
output[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// If the original JSON had these fields, log that we're overriding
|
||||
if _, hasTime := msgData[f.timestampField]; hasTime {
|
||||
f.logger.Debug("msg", "Overriding timestamp from JSON message",
|
||||
"component", "json_formatter",
|
||||
"original", msgData[f.timestampField],
|
||||
"logwisp", output[f.timestampField])
|
||||
}
|
||||
} else {
|
||||
// Message is not valid JSON - add as message field
|
||||
output[f.messageField] = entry.Message
|
||||
}
|
||||
|
||||
// Add any additional fields from LogEntry.Fields
|
||||
if len(entry.Fields) > 0 {
|
||||
var fields map[string]any
|
||||
if err := json.Unmarshal(entry.Fields, &fields); err == nil {
|
||||
// Merge additional fields, but don't override existing
|
||||
for k, v := range fields {
|
||||
if _, exists := output[k]; !exists {
|
||||
output[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal to JSON
|
||||
var result []byte
|
||||
var err error
|
||||
if f.pretty {
|
||||
result, err = json.MarshalIndent(output, "", " ")
|
||||
} else {
|
||||
result, err = json.Marshal(output)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal JSON: %w", err)
|
||||
}
|
||||
|
||||
// Add newline
|
||||
return append(result, '\n'), nil
|
||||
}
|
||||
|
||||
// Name returns the formatter name
|
||||
func (f *JSONFormatter) Name() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
// FormatBatch formats multiple entries as a JSON array
|
||||
// This is a special method for sinks that need to batch entries
|
||||
func (f *JSONFormatter) FormatBatch(entries []source.LogEntry) ([]byte, error) {
|
||||
// For batching, we need to create an array of formatted objects
|
||||
batch := make([]json.RawMessage, 0, len(entries))
|
||||
|
||||
for _, entry := range entries {
|
||||
// Format each entry without the trailing newline
|
||||
formatted, err := f.Format(entry)
|
||||
if err != nil {
|
||||
f.logger.Warn("msg", "Failed to format entry in batch",
|
||||
"component", "json_formatter",
|
||||
"error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove the trailing newline for array elements
|
||||
if len(formatted) > 0 && formatted[len(formatted)-1] == '\n' {
|
||||
formatted = formatted[:len(formatted)-1]
|
||||
}
|
||||
|
||||
batch = append(batch, formatted)
|
||||
}
|
||||
|
||||
// Marshal the entire batch as an array
|
||||
var result []byte
|
||||
var err error
|
||||
if f.pretty {
|
||||
result, err = json.MarshalIndent(batch, "", " ")
|
||||
} else {
|
||||
result, err = json.Marshal(batch)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
31
src/internal/format/raw.go
Normal file
31
src/internal/format/raw.go
Normal file
@ -0,0 +1,31 @@
|
||||
// FILE: src/internal/format/raw.go
|
||||
package format
|
||||
|
||||
import (
|
||||
"logwisp/src/internal/source"
|
||||
|
||||
"github.com/lixenwraith/log"
|
||||
)
|
||||
|
||||
// RawFormatter outputs the log message as-is with a newline
|
||||
type RawFormatter struct {
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewRawFormatter creates a new raw formatter
|
||||
func NewRawFormatter(options map[string]any, logger *log.Logger) (*RawFormatter, error) {
|
||||
return &RawFormatter{
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Format returns the message with a newline appended
|
||||
func (f *RawFormatter) Format(entry source.LogEntry) ([]byte, error) {
|
||||
// Simply return the message with newline
|
||||
return append([]byte(entry.Message), '\n'), nil
|
||||
}
|
||||
|
||||
// Name returns the formatter name
|
||||
func (f *RawFormatter) Name() string {
|
||||
return "raw"
|
||||
}
|
||||
108
src/internal/format/text.go
Normal file
108
src/internal/format/text.go
Normal file
@ -0,0 +1,108 @@
|
||||
// FILE: src/internal/format/text.go
|
||||
package format
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"logwisp/src/internal/source"
|
||||
|
||||
"github.com/lixenwraith/log"
|
||||
)
|
||||
|
||||
// TextFormatter produces human-readable text logs using templates
|
||||
type TextFormatter struct {
|
||||
template *template.Template
|
||||
timestampFormat string
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewTextFormatter creates a new text formatter
|
||||
func NewTextFormatter(options map[string]any, logger *log.Logger) (*TextFormatter, error) {
|
||||
// Default template
|
||||
templateStr := "[{{.Timestamp | FmtTime}}] [{{.Level | ToUpper}}] {{.Source}} - {{.Message}}{{ if .Fields }} {{.Fields}}{{ end }}"
|
||||
if tmpl, ok := options["template"].(string); ok && tmpl != "" {
|
||||
templateStr = tmpl
|
||||
}
|
||||
|
||||
// Default timestamp format
|
||||
timestampFormat := time.RFC3339
|
||||
if tsFormat, ok := options["timestamp_format"].(string); ok && tsFormat != "" {
|
||||
timestampFormat = tsFormat
|
||||
}
|
||||
|
||||
f := &TextFormatter{
|
||||
timestampFormat: timestampFormat,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Create template with helper functions
|
||||
funcMap := template.FuncMap{
|
||||
"FmtTime": func(t time.Time) string {
|
||||
return t.Format(f.timestampFormat)
|
||||
},
|
||||
"ToUpper": strings.ToUpper,
|
||||
"ToLower": strings.ToLower,
|
||||
"TrimSpace": strings.TrimSpace,
|
||||
}
|
||||
|
||||
tmpl, err := template.New("log").Funcs(funcMap).Parse(templateStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid template: %w", err)
|
||||
}
|
||||
|
||||
f.template = tmpl
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Format formats the log entry using the template
|
||||
func (f *TextFormatter) Format(entry source.LogEntry) ([]byte, error) {
|
||||
// Prepare data for template
|
||||
data := map[string]any{
|
||||
"Timestamp": entry.Time,
|
||||
"Level": entry.Level,
|
||||
"Source": entry.Source,
|
||||
"Message": entry.Message,
|
||||
}
|
||||
|
||||
// Set default level if empty
|
||||
if data["Level"] == "" {
|
||||
data["Level"] = "INFO"
|
||||
}
|
||||
|
||||
// Add fields if present
|
||||
if len(entry.Fields) > 0 {
|
||||
data["Fields"] = string(entry.Fields)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := f.template.Execute(&buf, data); err != nil {
|
||||
// Fallback: return a basic formatted message
|
||||
f.logger.Debug("msg", "Template execution failed, using fallback",
|
||||
"component", "text_formatter",
|
||||
"error", err)
|
||||
|
||||
fallback := fmt.Sprintf("[%s] [%s] %s - %s\n",
|
||||
entry.Time.Format(f.timestampFormat),
|
||||
strings.ToUpper(entry.Level),
|
||||
entry.Source,
|
||||
entry.Message)
|
||||
return []byte(fallback), nil
|
||||
}
|
||||
|
||||
// Ensure newline at end
|
||||
result := buf.Bytes()
|
||||
if len(result) == 0 || result[len(result)-1] != '\n' {
|
||||
result = append(result, '\n')
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Name returns the formatter name
|
||||
func (f *TextFormatter) Name() string {
|
||||
return "text"
|
||||
}
|
||||
Reference in New Issue
Block a user