From 4534b4d8ca8288a66996a5ed4c24ee0305f282ba Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 26 May 2024 21:28:34 +0300 Subject: [PATCH 1/5] Add more log customization options --- internal/app/app.go | 10 +++++++- internal/app/log.go | 59 +++++++++++++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 090c023a..3ccb5f44 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -13,6 +13,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/AlexxIT/go2rtc/pkg/yaml" + "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) @@ -117,9 +118,16 @@ func Init() { Mod map[string]string `yaml:"log"` } + cfg.Mod = map[string]string{ + "format": "color", + "level": zerolog.LevelInfoValue, + "output": "stdout", // TODO: change to stderr someday + "time": zerolog.TimeFormatUnixMs, + } + LoadConfig(&cfg) - log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"]) + log.Logger = NewLogger(cfg.Mod) modules = cfg.Mod diff --git a/internal/app/log.go b/internal/app/log.go index e8d4bc88..c32de685 100644 --- a/internal/app/log.go +++ b/internal/app/log.go @@ -8,29 +8,58 @@ import ( "github.com/rs/zerolog/log" ) -var MemoryLog *circularBuffer +var MemoryLog = newBuffer(16) -func NewLogger(format string, level string) zerolog.Logger { - var writer io.Writer = os.Stdout +func NewLogger(config map[string]string) zerolog.Logger { + var writer io.Writer - if format != "json" { - writer = zerolog.ConsoleWriter{ - Out: writer, TimeFormat: "15:04:05.000", NoColor: format == "text", + // support output only to memory + switch config["output"] { + case "stderr": + writer = os.Stderr + case "stdout": + writer = os.Stdout + } + + timeFormat := config["time"] + + if writer != nil { + switch format := config["format"]; format { + case "color", "text": + if timeFormat != "" { + writer = &zerolog.ConsoleWriter{ + Out: writer, + NoColor: format == "text", + TimeFormat: "15:04:05.000", + } + } else { + writer = &zerolog.ConsoleWriter{ + Out: writer, + NoColor: format == "text", + PartsOrder: []string{ + zerolog.LevelFieldName, + zerolog.CallerFieldName, + zerolog.MessageFieldName, + }, + } + } + case "json": // none } + + writer = zerolog.MultiLevelWriter(writer, MemoryLog) + } else { + writer = MemoryLog } - MemoryLog = newBuffer(16) + logger := zerolog.New(writer) - writer = zerolog.MultiLevelWriter(writer, MemoryLog) - - zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs - - lvl, err := zerolog.ParseLevel(level) - if err != nil || lvl == zerolog.NoLevel { - lvl = zerolog.InfoLevel + if timeFormat != "" { + zerolog.TimeFieldFormat = timeFormat + logger = logger.With().Timestamp().Logger() } - return zerolog.New(writer).With().Timestamp().Logger().Level(lvl) + lvl, _ := zerolog.ParseLevel(config["level"]) + return logger.Level(lvl) } func GetLogger(module string) zerolog.Logger { From 3932dbaa84afaf1ba8dd3ade5dcd5240c67c6db5 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 27 May 2024 20:23:55 +0300 Subject: [PATCH 2/5] Add print exec stderr to logs for debug level --- internal/exec/exec.go | 59 ++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 1e11cc68..454c54a4 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -4,7 +4,7 @@ import ( "crypto/md5" "encoding/hex" "errors" - "io" + "fmt" "net/url" "os" "os/exec" @@ -68,8 +68,9 @@ func execHandle(rawURL string) (core.Producer, error) { args := shell.QuoteSplit(rawURL[5:]) // remove `exec:` cmd := exec.Command(args[0], args[1:]...) - if log.Debug().Enabled() { - cmd.Stderr = os.Stderr + cmd.Stderr = &logWriter{ + buf: make([]byte, 512), + debug: log.Debug().Enabled(), } if path == "" { @@ -104,18 +105,10 @@ func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe") - return prod, err + return prod, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr) } func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { - stderr := limitBuffer{buf: make([]byte, 512)} - - if cmd.Stderr != nil { - cmd.Stderr = io.MultiWriter(cmd.Stderr, &stderr) - } else { - cmd.Stderr = &stderr - } - if log.Trace().Enabled() { cmd.Stdout = os.Stdout } @@ -150,10 +143,10 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { case <-time.After(time.Second * 60): _ = cmd.Process.Kill() log.Error().Str("url", url).Msg("[exec] timeout") - return nil, errors.New("timeout") + return nil, errors.New("exec: timeout") case <-done: // limit message size - return nil, errors.New("exec: " + stderr.String()) + return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr) case prod := <-waiter: log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp") return prod, nil @@ -168,21 +161,47 @@ var ( waitersMu sync.Mutex ) -type limitBuffer struct { - buf []byte - n int +type logWriter struct { + buf []byte + debug bool + n int } -func (l *limitBuffer) String() string { +func (l *logWriter) String() string { if l.n == len(l.buf) { return string(l.buf) + "..." } return string(l.buf[:l.n]) } -func (l *limitBuffer) Write(p []byte) (int, error) { +func (l *logWriter) Write(p []byte) (n int, err error) { if l.n < cap(l.buf) { l.n += copy(l.buf[l.n:], p) } - return len(p), nil + n = len(p) + if l.debug { + if p = trimSpace(p); p != nil { + log.Debug().Msgf("[exec] %s", p) + } + } + return +} + +func trimSpace(b []byte) []byte { + start := 0 + stop := len(b) + for ; start < stop; start++ { + if b[start] >= ' ' { + break // trim all ASCII before 0x20 + } + } + for ; ; stop-- { + if stop == start { + return nil // skip empty output + } + if b[stop-1] > ' ' { + break // trim all ASCII before 0x21 + } + } + return b[start:stop] } From 8cb513cb89b4739ccf86bde5bff79264d71a539f Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 27 May 2024 20:24:24 +0300 Subject: [PATCH 3/5] Add log level for ffmpeg module --- internal/app/app.go | 2 +- internal/ffmpeg/ffmpeg.go | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 3ccb5f44..add11dd7 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -120,7 +120,7 @@ func Init() { cfg.Mod = map[string]string{ "format": "color", - "level": zerolog.LevelInfoValue, + "level": "info", "output": "stdout", // TODO: change to stderr someday "time": zerolog.TimeFormatUnixMs, } diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 2b24c3ce..aeba85fb 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -19,15 +19,22 @@ import ( func Init() { var cfg struct { Mod map[string]string `yaml:"ffmpeg"` + Log struct { + Level string `yaml:"ffmpeg"` + } `yaml:"log"` } cfg.Mod = defaults // will be overriden from yaml + cfg.Log.Level = "error" app.LoadConfig(&cfg) - if app.GetLogger("exec").GetLevel() >= 0 { - defaults["global"] += " -v error" + // zerolog levels: trace debug info warn error fatal panic disabled + // FFmpeg levels: trace debug verbose info warning error fatal panic quiet + if cfg.Log.Level == "warn" { + cfg.Log.Level = "warning" } + defaults["global"] += " -v " + cfg.Log.Level streams.RedirectFunc("ffmpeg", func(url string) (string, error) { if _, err := Version(); err != nil { From 649de0131cbaf64bd3d3f11cf575e0d630f785b3 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 27 May 2024 20:25:09 +0300 Subject: [PATCH 4/5] Change logs timestamp format in WebUI --- www/log.html | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/www/log.html b/www/log.html index e3aff5d5..84ec0675 100644 --- a/www/log.html +++ b/www/log.html @@ -56,7 +56,7 @@ - + @@ -98,11 +98,16 @@ lines = lines.reverse(); } return lines.map(line => { - const ts = new Date(line['time']); + const ts = new Date(line['time']).toLocaleString(undefined, { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + fractionalSecondDigits: 3 + }); const msg = Object.keys(line).reduce((msg, key) => { return KEYS.indexOf(key) < 0 ? `${msg} ${key}=${line[key]}` : msg; }, line['message']); - return ``; + return ``; }).join(''); } From 50ad3b20c4c7c3258d7b1c56db912e3afbd3c5d8 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 28 May 2024 09:08:57 +0300 Subject: [PATCH 5/5] Add config schema.json --- website/schema.json | 486 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 486 insertions(+) create mode 100644 website/schema.json diff --git a/website/schema.json b/website/schema.json new file mode 100644 index 00000000..d5e19436 --- /dev/null +++ b/website/schema.json @@ -0,0 +1,486 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "go2rtc", + "type": "object", + "additionalProperties": false, + "definitions": { + "listen": { + "type": "string", + "anyOf": [ + { + "type": "string", + "pattern": ":[0-9]{1,5}$" + }, + { + "type": "string", + "const": "" + } + ] + }, + "log_level": { + "type": "string", + "enum": [ + "trace", + "debug", + "info", + "warn", + "error" + ] + } + }, + "properties": { + "api": { + "type": "object", + "properties": { + "listen": { + "default": ":1984", + "examples": [ + "127.0.0.1:8080" + ], + "$ref": "#/definitions/listen" + }, + "username": { + "type": "string", + "examples": [ + "admin" + ] + }, + "password": { + "type": "string" + }, + "base_path": { + "type": "string", + "examples": [ + "/go2rtc" + ] + }, + "static_dir": { + "type": "string", + "examples": [ + "/var/www" + ] + }, + "origin": { + "type": "string", + "const": "*" + }, + "tls_listen": { + "$ref": "#/definitions/listen" + }, + "tls_cert": { + "type": "string", + "examples": [ + "-----BEGIN CERTIFICATE-----", + "/ssl/fullchain.pem" + ] + }, + "tls_key": { + "type": "string", + "examples": [ + "-----BEGIN PRIVATE KEY-----", + "/ssl/privkey.pem" + ] + }, + "unix_listen": { + "type": "string", + "examples": [ + "/tmp/go2rtc.sock" + ] + } + } + }, + "ffmpeg": { + "type": "object", + "properties": { + "bin": { + "type": "string", + "default": "ffmpeg" + } + }, + "additionalProperties": { + "description": "FFmpeg template", + "type": "string" + } + }, + "hass": { + "type": "object", + "properties": { + "config": { + "description": "Home Assistant config directory path", + "type": "string", + "examples": [ + "/config" + ] + } + } + }, + "homekit": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "pin": { + "type": "string", + "default": "19550224", + "pattern": "^[0-9]{8}$" + }, + "name": { + "type": "string" + }, + "device_id": { + "type": "string" + }, + "device_private": { + "type": "string" + }, + "pairings": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "log": { + "type": "object", + "properties": { + "format": { + "type": "string", + "default": "color", + "enum": [ + "color", + "json", + "text" + ] + }, + "level": { + "description": "Defaul log level", + "default": "info", + "$ref": "#/definitions/log_level" + }, + "output": { + "type": "string", + "default": "stdout", + "enum": [ + "", + "stdout", + "stderr" + ] + }, + "time": { + "type": "string", + "default": "UNIXMS", + "anyOf": [ + { + "type": "string", + "enum": [ + "", + "UNIXMS", + "UNIXMICRO", + "UNIXNANO", + "2006-01-02T15:04:05Z07:00", + "2006-01-02T15:04:05.999999999Z07:00" + ] + }, + { + "type": "string" + } + ] + }, + "api": { + "$ref": "#/definitions/log_level" + }, + "echo": { + "$ref": "#/definitions/log_level" + }, + "exec": { + "description": "Value `exec: debug` will print stderr", + "$ref": "#/definitions/log_level" + }, + "expr": { + "$ref": "#/definitions/log_level" + }, + "ffmpeg": { + "description": "Will only be displayed with `exec: debug` setting", + "default": "error", + "$ref": "#/definitions/log_level" + }, + "hass": { + "$ref": "#/definitions/log_level" + }, + "hls": { + "$ref": "#/definitions/log_level" + }, + "homekit": { + "$ref": "#/definitions/log_level" + }, + "mp4": { + "$ref": "#/definitions/log_level" + }, + "ngrok": { + "$ref": "#/definitions/log_level" + }, + "onvif": { + "$ref": "#/definitions/log_level" + }, + "rtmp": { + "$ref": "#/definitions/log_level" + }, + "rtsp": { + "$ref": "#/definitions/log_level" + }, + "streams": { + "$ref": "#/definitions/log_level" + }, + "webrtc": { + "$ref": "#/definitions/log_level" + }, + "webtorrent": { + "$ref": "#/definitions/log_level" + } + } + }, + "ngrok": { + "type": "object", + "properties": { + "command": { + "type": "string", + "examples": [ + "ngrok tcp 8555 --authtoken xxx", + "ngrok start --all --config ngrok.yaml" + ] + } + } + }, + "publish": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "examples": [ + "rtmp://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx", + "rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx" + ] + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "rtmp": { + "type": "object", + "properties": { + "listen": { + "examples": [ + ":1935" + ], + "$ref": "#/definitions/listen" + } + } + }, + "rtsp": { + "type": "object", + "properties": { + "listen": { + "default": ":8554", + "$ref": "#/definitions/listen" + }, + "username": { + "type": "string", + "examples": [ + "admin" + ] + }, + "password": { + "type": "string" + }, + "default_query": { + "type": "string", + "default": "video&audio" + }, + "pkt_size": { + "type": "integer" + } + } + }, + "srtp": { + "description": "SRTP server for HomeKit", + "type": "object", + "properties": { + "listen": { + "default": ":8443", + "$ref": "#/definitions/listen" + } + } + }, + "streams": { + "type": "object", + "additionalProperties": { + "title": "Stream", + "anyOf": [ + { + "description": "Source", + "type": "string", + "examples": [ + "rtsp://username:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif", + "rtsp://username:password@192.168.1.123/stream1", + "rtsp://username:password@192.168.1.123/h264Preview_01_main", + "rtmp://192.168.1.123/bcs/channel0_main.bcs?channel=0&stream=0&user=username&password=password", + "http://192.168.1.123/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password", + "http://username:password@192.168.1.123/cgi-bin/snapshot.cgi?channel=1", + "ffmpeg:media.mp4#video=h264#hadware#width=1920#height=1080#rotate=180#audio=copy", + "ffmpeg:virtual?video=testsrc&size=4K#video=h264#hardware#bitrate=50M", + "bubble://username:password@192.168.1.123:34567/bubble/live?ch=0&stream=0", + "dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0", + "exec:ffmpeg -re -i media.mp4 -c copy -rtsp_transport tcp -f rtsp {output}", + "isapi://username:password@192.168.1.123:80/", + "kasa://username:password@192.168.1.123:19443/https/stream/mixed", + "onvif://username:password@192.168.1.123:80?subtype=0", + "tapo://password@192.168.1.123:8800?channel=0&subtype=0", + "webtorrent:?share=xxx&pwd=xxx" + ] + }, + { + "type": "array", + "items": { + "description": "Source", + "type": "string" + } + } + ] + } + }, + "webrtc": { + "type": "object", + "properties": { + "listen": { + "default": ":8555/tcp", + "type": "string", + "anyOf": [ + { + "type": "string", + "pattern": ":[0-9]{1,5}(/tcp|/udp)?$" + }, + { + "type": "string", + "const": "" + } + ] + }, + "candidates": { + "type": "array", + "items": { + "$ref": "#/definitions/listen/anyOf/0" + }, + "examples": [ + "216.58.210.174:8555", + "stun:8555", + "home.duckdns.org:8555" + ] + }, + "ice_servers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "urls": { + "type": "array", + "items": { + "type": "string", + "examples": [ + "stun:stun.l.google.com:19302", + "turn:123.123.123.123:3478" + ] + } + }, + "username": { + "type": "string" + }, + "credential": { + "type": "string" + } + } + } + }, + "filters": { + "type": "object", + "properties": { + "candidates": { + "description": "Keep only these candidates", + "type": "array", + "items": { + "type": "string" + } + }, + "interfaces": { + "description": "Keep only these interfaces", + "type": "array", + "items": { + "type": "string" + } + }, + "ips": { + "description": "Keep only these IP-addresses", + "type": "array", + "items": { + "type": "string" + } + }, + "networks": { + "description": "Use only these network types", + "type": "array", + "items": { + "enum": [ + "tcp4", + "tcp6", + "udp4", + "udp6" + ], + "type": "string" + } + }, + "udp_ports": { + "description": "Use only these UDP ports range [min, max]", + "type": "array", + "items": { + "type": "integer" + }, + "maxItems": 2, + "minItems": 2 + } + } + } + } + }, + "webtorrent": { + "type": "object", + "properties": { + "trackers": { + "type": "array", + "items": { + "type": "string" + } + }, + "shares": { + "additionalProperties": { + "type": "object", + "properties": { + "pwd": { + "type": "string" + }, + "src": { + "type": "string" + } + } + } + } + } + } + } +} \ No newline at end of file
TimeTime Level Message
${ts.toLocaleString()}${escapeHTML(line['level'])}${escapeHTML(msg)}
${ts}${line['level']}${escapeHTML(msg)}