fix: order command controls after dispatch
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package observability
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RotatingFile is an append-only daemon log with bounded numbered archives.
|
||||
// It is deliberately a single-process writer: deployment supplies one daemon
|
||||
// process per configured path, and a second writer must use a distinct file.
|
||||
// Archive .1 is newest; .maxFiles is oldest.
|
||||
type RotatingFile struct {
|
||||
mu sync.Mutex
|
||||
path string
|
||||
maxBytes uint64
|
||||
maxFiles uint32
|
||||
file *os.File
|
||||
size uint64
|
||||
}
|
||||
|
||||
// OpenRotatingFile opens path for append, creating its parent directories with
|
||||
// conservative permissions. A blank path disables file logging and returns a
|
||||
// no-op closer. Both rotation controls must be zero (unbounded) or positive.
|
||||
func OpenRotatingFile(path string, maxBytes uint64, maxFiles uint32) (io.WriteCloser, error) {
|
||||
if path == "" {
|
||||
return nopWriteCloser{Writer: io.Discard}, nil
|
||||
}
|
||||
if (maxBytes == 0) != (maxFiles == 0) {
|
||||
return nil, errors.New("log rotation byte/file limits must both be zero or positive")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return nil, fmt.Errorf("create log directory: %w", err)
|
||||
}
|
||||
result := &RotatingFile{path: path, maxBytes: maxBytes, maxFiles: maxFiles}
|
||||
if err := result.open(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (file *RotatingFile) open() error {
|
||||
opened, err := os.OpenFile(file.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open log file: %w", err)
|
||||
}
|
||||
info, err := opened.Stat()
|
||||
if err != nil {
|
||||
_ = opened.Close()
|
||||
return fmt.Errorf("stat log file: %w", err)
|
||||
}
|
||||
file.file = opened
|
||||
file.size = uint64(info.Size())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (file *RotatingFile) Write(data []byte) (int, error) {
|
||||
file.mu.Lock()
|
||||
defer file.mu.Unlock()
|
||||
if file.file == nil {
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
if file.maxBytes != 0 && file.size != 0 && (file.size >= file.maxBytes || uint64(len(data)) > file.maxBytes-file.size) {
|
||||
if err := file.rotate(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
written, err := file.file.Write(data)
|
||||
file.size += uint64(written)
|
||||
return written, err
|
||||
}
|
||||
|
||||
func (file *RotatingFile) Close() error {
|
||||
file.mu.Lock()
|
||||
defer file.mu.Unlock()
|
||||
if file.file == nil {
|
||||
return nil
|
||||
}
|
||||
err := file.file.Close()
|
||||
file.file = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (file *RotatingFile) rotate() error {
|
||||
if err := file.file.Close(); err != nil {
|
||||
return fmt.Errorf("close log before rotation: %w", err)
|
||||
}
|
||||
file.file = nil
|
||||
oldest := file.archivePath(file.maxFiles)
|
||||
if err := os.Remove(oldest); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("remove oldest log archive: %w", err)
|
||||
}
|
||||
for index := file.maxFiles; index > 1; index-- {
|
||||
from := file.archivePath(index - 1)
|
||||
to := file.archivePath(index)
|
||||
if err := os.Rename(from, to); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("rotate log archive: %w", err)
|
||||
}
|
||||
}
|
||||
if err := os.Rename(file.path, file.archivePath(1)); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("seal current log: %w", err)
|
||||
}
|
||||
return file.open()
|
||||
}
|
||||
|
||||
func (file *RotatingFile) archivePath(index uint32) string {
|
||||
return file.path + "." + strconv.FormatUint(uint64(index), 10)
|
||||
}
|
||||
|
||||
type nopWriteCloser struct{ io.Writer }
|
||||
|
||||
func (nopWriteCloser) Close() error { return nil }
|
||||
|
||||
// FormatLog makes file records either newline-delimited JSON or plain text.
|
||||
// It is intentionally applied only to daemon diagnostics, never command
|
||||
// output, stdin, environment values, or other payload-bearing data.
|
||||
func FormatLog(destination io.Writer, format string) io.Writer {
|
||||
if destination == nil || format != "json" {
|
||||
return destination
|
||||
}
|
||||
return &jsonLogWriter{destination: destination}
|
||||
}
|
||||
|
||||
type jsonLogWriter struct {
|
||||
mu sync.Mutex
|
||||
destination io.Writer
|
||||
}
|
||||
|
||||
func (writer *jsonLogWriter) Write(data []byte) (int, error) {
|
||||
writer.mu.Lock()
|
||||
defer writer.mu.Unlock()
|
||||
for _, line := range bytes.Split(data, []byte{'\n'}) {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
record, err := json.Marshal(struct {
|
||||
Time string `json:"time"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
}{Time: time.Now().UTC().Format(time.RFC3339Nano), Level: "info", Message: string(line)})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("encode structured log: %w", err)
|
||||
}
|
||||
if _, err := writer.destination.Write(append(record, '\n')); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return len(data), nil
|
||||
}
|
||||
Reference in New Issue
Block a user