Log files: one file per entry, format detection
- a path is one file (wildcards refused for now); the name defaults to
the file name
- format "auto" (the default): detected from the last 50 lines of the
file, the structured format followed by at least 80 % of them (Traefik
access JSON, JSON lines, CLF, logfmt), else plain text; done again when
the format changes, shown in the status ("Format detected: …"); any
other format forces it
- POST /api/files/detect {path}: found / permission / empty, detected
format and last line, shown under the row as soon as the path is typed
(POST so that read-only accounts cannot read files through it)
- README (EN/FR) updated
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
087d9e7b30
commit
dcb09e619e
8 files changed
+488
-284
No files matched your search
+100
@@ -1,8 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -12,6 +15,7 @@ import (
|
||||
|
||||
// Formats of the "Files" source (Settings > Sources), one per declared file.
|
||||
const (
|
||||
fmtAuto = "auto" // detected from the last lines of the file
|
||||
fmtText = "text" // one line per entry, level and leading timestamp detected
|
||||
fmtJSON = "json" // JSON lines: msg/time/level recognized, other keys as fields
|
||||
fmtLogfmt = "logfmt" // key=value pairs (Traefik "common" logs, Go programs…)
|
||||
@@ -587,3 +591,99 @@ func traefikRec(obj map[string]any, line string, now time.Time) *fileRec {
|
||||
r.msg = b.String()
|
||||
return r
|
||||
}
|
||||
|
||||
/* ---------- Detection ---------- */
|
||||
|
||||
const detectWindow = 64 << 10
|
||||
|
||||
// sampleLines returns the last complete lines of a file (50 at most).
|
||||
func sampleLines(path string) ([]string, error) {
|
||||
fh, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
fi, err := fh.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
off := fi.Size() - detectWindow
|
||||
if off < 0 {
|
||||
off = 0
|
||||
}
|
||||
buf, err := io.ReadAll(io.NewSectionReader(fh, off, detectWindow))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
end := bytes.LastIndexByte(buf, '\n')
|
||||
if end < 0 {
|
||||
return nil, nil
|
||||
}
|
||||
buf = buf[:end]
|
||||
if off > 0 { // first line cut by the window
|
||||
if nl := bytes.IndexByte(buf, '\n'); nl >= 0 {
|
||||
buf = buf[nl+1:]
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
var lines []string
|
||||
for _, b := range bytes.Split(buf, []byte{'\n'}) {
|
||||
if len(bytes.TrimSpace(b)) > 0 {
|
||||
lines = append(lines, string(bytes.TrimRight(b, "\r")))
|
||||
}
|
||||
}
|
||||
if len(lines) > 50 {
|
||||
lines = lines[len(lines)-50:]
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
// logfmtKeys: the pairs have one of the usual logfmt keys.
|
||||
func logfmtKeys(kv [][2]string) bool {
|
||||
for _, p := range kv {
|
||||
switch strings.ToLower(p[0]) {
|
||||
case "level", "lvl", "msg", "message", "time", "ts", "t":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// detectFormat guesses the format of a file from some of its lines: the
|
||||
// structured format most lines follow, else plain text (a regular
|
||||
// expression is never guessed).
|
||||
func detectFormat(lines []string) string {
|
||||
var js, traefik, clf, logfmt int
|
||||
for _, l := range lines {
|
||||
l = strings.TrimSpace(l)
|
||||
if obj, ok := decodeJSONLine(l); ok {
|
||||
js++
|
||||
if _, ok := obj["RequestMethod"]; ok {
|
||||
traefik++
|
||||
} else if _, ok := obj["DownstreamStatus"]; ok {
|
||||
traefik++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if clfRe.MatchString(l) {
|
||||
clf++
|
||||
continue
|
||||
}
|
||||
if kv := parseLogfmt(l); len(kv) >= 3 && logfmtKeys(kv) {
|
||||
logfmt++
|
||||
}
|
||||
}
|
||||
most := func(n int) bool { return n > 0 && n*5 >= len(lines)*4 } // 80 %
|
||||
switch {
|
||||
case most(js) && traefik*2 >= js:
|
||||
return fmtTraefik
|
||||
case most(js):
|
||||
return fmtJSON
|
||||
case most(clf):
|
||||
return fmtCLF
|
||||
case most(logfmt):
|
||||
return fmtLogfmt
|
||||
}
|
||||
return fmtText
|
||||
}
|
||||
Reference in new issue
Block a user