PrintToVeraeTimeStamp/internal/vlog/vlog.go

144 lines
3.7 KiB
Go

// Package vlog is the verbose debug logger used by the CLI, the print helper
// and the IPP/watch servers. On Windows logs go to %ProgramData%\Verae\logs
// (shared, ACL'd by the installer so Users can write); elsewhere to the user
// cache dir. Every print job carries a job ID so one job can be followed from
// IPP receive → save → hash → API → certificate.
package vlog
import (
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
)
var (
mu sync.Mutex
out io.Writer = os.Stderr
file *os.File
comp = "verae"
jobSeq uint64
debugOn = os.Getenv("VERAE_DEBUG") == "1"
)
// Dir returns the log directory for this platform.
func Dir() string {
if runtime.GOOS == "windows" {
pd := os.Getenv("ProgramData")
if pd == "" {
pd = `C:\ProgramData`
}
return filepath.Join(pd, "Verae", "logs")
}
if d, err := os.UserCacheDir(); err == nil {
return filepath.Join(d, "Verae", "logs")
}
return filepath.Join(os.TempDir(), "verae-logs")
}
func userTag() string {
if u, err := user.Current(); err == nil {
n := u.Username
if i := strings.LastIndexAny(n, `\/`); i >= 0 {
n = n[i+1:]
}
return sanitize(n)
}
return "unknown"
}
func sanitize(s string) string {
var b strings.Builder
for _, r := range s {
if r < 128 && (r == '-' || r == '_' || r == '.' || (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')) {
b.WriteRune(r)
}
}
if b.Len() == 0 {
return "x"
}
return b.String()
}
// Init opens <Dir>/<component>-<user>.log (append). alsoStderr tees output.
// Falls back to %TEMP% if the shared dir is not writable. Returns the path.
func Init(component string, alsoStderr bool) string {
mu.Lock()
defer mu.Unlock()
comp = component
name := fmt.Sprintf("%s-%s.log", component, userTag())
var path string
for _, d := range []string{Dir(), filepath.Join(os.TempDir(), "Verae", "logs")} {
if err := os.MkdirAll(d, 0o775); err != nil {
continue
}
p := filepath.Join(d, name)
f, err := os.OpenFile(p, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o664)
if err != nil {
continue
}
file, path = f, p
break
}
switch {
case file != nil && alsoStderr:
out = io.MultiWriter(file, os.Stderr)
case file != nil:
out = file
default:
out = os.Stderr
}
return path
}
// SetDebug enables verbose (Debugf) output.
func SetDebug(on bool) { debugOn = on || debugOn }
// DebugEnabled reports whether verbose output is on.
func DebugEnabled() bool { return debugOn }
func write(level, job, msg string) {
mu.Lock()
defer mu.Unlock()
ts := time.Now().Format("2006-01-02T15:04:05.000-07:00")
if job == "" {
job = "-"
}
fmt.Fprintf(out, "%s %s %s pid=%d job=%s %s\n", ts, level, comp, os.Getpid(), job, strings.TrimRight(msg, "\n"))
}
// Printf logs at INFO with no job.
func Printf(format string, a ...any) { write("INFO", "", fmt.Sprintf(format, a...)) }
// Errorf logs at ERROR with no job.
func Errorf(format string, a ...any) { write("ERROR", "", fmt.Sprintf(format, a...)) }
// Debugf logs at DEBUG when enabled.
func Debugf(format string, a ...any) {
if debugOn {
write("DEBUG", "", fmt.Sprintf(format, a...))
}
}
// NewJobID returns J<yyyymmdd-hhmmss>-<src>-<n>.
func NewJobID(src string) string {
n := atomic.AddUint64(&jobSeq, 1)
return fmt.Sprintf("J%s-%s-%d", time.Now().Format("20060102-150405"), sanitize(src), n)
}
// Job is a logger bound to a job ID.
type Job string
func (j Job) Printf(format string, a ...any) { write("INFO", string(j), fmt.Sprintf(format, a...)) }
func (j Job) Errorf(format string, a ...any) { write("ERROR", string(j), fmt.Sprintf(format, a...)) }
func (j Job) Debugf(format string, a ...any) {
if debugOn {
write("DEBUG", string(j), fmt.Sprintf(format, a...))
}
}