homelab/services/ha-sync/internal/opslog/writer.go
Dan V deb6c38d7b chore: commit homelab setup — deployment, services, orchestration, skill
- Add .gitignore: exclude compiled binaries, build artifacts, and Helm
  values files containing real secrets (authentik, prometheus)
- Add all Kubernetes deployment manifests (deployment/)
- Add services source code: ha-sync, device-inventory, games-console,
  paperclip, parts-inventory
- Add Ansible orchestration: playbooks, roles, inventory, cloud-init
- Add hardware specs, execution plans, scripts, HOMELAB.md
- Add skills/homelab/SKILL.md + skills/install.sh to preserve Copilot skill
- Remove previously-tracked inventory-cli binary from git index

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 08:10:32 +02:00

86 lines
2.5 KiB
Go

package opslog
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
)
// LogEntry is the detailed record written to .jsonl files and flushed to the DB.
type LogEntry struct {
IterationID int64 `json:"iteration_id"`
DryRun bool `json:"dry_run"`
Operation string `json:"operation"`
Filepath string `json:"filepath"`
SizeBefore int64 `json:"size_before"`
SizeAfter int64 `json:"size_after"`
MD5Before string `json:"md5_before"`
MD5After string `json:"md5_after"`
StartedAt time.Time `json:"started_at"`
EndedAt time.Time `json:"ended_at"`
Status string `json:"status"`
ErrorMessage string `json:"error_message"`
}
// OpRecord describes a single file operation performed during a sync pass.
type OpRecord struct {
IterID int64 `json:"iter_id"`
RelPath string `json:"rel_path"`
Action string `json:"action"` // create | update | delete | skip | fail
Bytes int64 `json:"bytes,omitempty"`
ErrMsg string `json:"error,omitempty"`
At time.Time `json:"at"`
Owner string `json:"owner,omitempty"`
}
// Writer serialises OpRecords as newline-delimited JSON.
type Writer struct {
file *os.File // non-nil when opened via Open
enc *json.Encoder
}
// NewWriter wraps any io.Writer (useful for testing).
func NewWriter(w io.Writer) *Writer {
return &Writer{enc: json.NewEncoder(w)}
}
// Open creates logDir if needed and opens a new .jsonl log file named after
// pair, direction and the current UTC timestamp.
func Open(logDir, pair, direction string) (*Writer, error) {
if err := os.MkdirAll(logDir, 0o755); err != nil {
return nil, fmt.Errorf("opslog: create dir: %w", err)
}
ts := strings.ReplaceAll(time.Now().UTC().Format(time.RFC3339), ":", "-")
name := fmt.Sprintf("opslog-%s-%s-%s.jsonl", pair, direction, ts)
f, err := os.Create(filepath.Join(logDir, name))
if err != nil {
return nil, fmt.Errorf("opslog: open file: %w", err)
}
return &Writer{file: f, enc: json.NewEncoder(f)}, nil
}
// WriteOp stamps rec with the current UTC time and encodes it as JSON.
func (w *Writer) WriteOp(rec OpRecord) error {
rec.At = time.Now().UTC()
return w.enc.Encode(rec)
}
// Close closes the underlying file if the Writer was created with Open.
func (w *Writer) Close() error {
if w.file != nil {
return w.file.Close()
}
return nil
}
// Path returns the log file path when the Writer was created with Open.
func (w *Writer) Path() string {
if w.file != nil {
return w.file.Name()
}
return ""
}