Files
liminix/pkgs/logshippers/victorialogsend.fnl
2025-10-08 20:10:11 +01:00

101 lines
2.8 KiB
Fennel

(local { : base64 : assoc } (require :anoia))
(local tai64 (require :anoia.tai64))
(local ll (require :lualinux))
(import-macros { : expect= : define-tests } :anoia.assert)
(local crlf "\r\n")
(fn chunk [str]
(let [len (# str)]
(string.format "%x%s%s%s" len crlf str crlf)))
(fn parse-url [str]
;; this is a very poor parser as it won't recognise
;; credentials in the authority and it lumps query-string/fragment
;; into the path
(let [(scheme host path)
(string.match str "(.-)://(.-)(/.+)")]
{ : scheme : host : path }))
(define-tests
(expect= (parse-url "https://www.example.com/stairway/to/heaven")
{ :scheme "https"
:host "www.example.com"
:path "/stairway/to/heaven"
}))
(fn parse-args [args]
(case args
["--basic-auth" auth & rest]
(assoc (parse-args rest) :auth auth)
[url] { :url (parse-url url) }
_ (error "invalid args")))
(fn http-header [host path auth]
(let [b64 (base64 :url)
authstr
(if auth
(string.format "Authorization: basic %s\n" (b64:encode auth))
"")]
(string.format
"POST %s HTTP/1.1\r
Host: %s\
%sTransfer-Encoding: chunked\r
\r
"
path host authstr)))
(fn format-timestamp-rfc3339 [timestamp prec]
(let [(sec nano) (-> timestamp tai64.from-timestamp tai64.to-utc)
subsec (string.sub (string.format "%09d" nano) 1 prec)]
(.. (os.date "!%FT%T" sec)
"." subsec
"Z")))
(define-tests
(expect= (format-timestamp-rfc3339 "@4000000068e2f0d3257dc09b" 9)
"2025-10-05T22:26:54.628998299Z")
(expect= (format-timestamp-rfc3339 "@4000000068e2f0d3257dc09b" 3)
"2025-10-05T22:26:54.628Z"))
(fn process-line [line]
(let [(timestamp hostname service msg) (string.match line "(@%x+) (%g+) (%g+) (.+)$")]
(->
(if timestamp
(string.format
"{%q:%q,%q:%q,%q:%q,%q:%q}\n"
:_time (format-timestamp-rfc3339 timestamp 3)
:service service
:_msg msg
:host hostname)
(string.format
"{%q:%q,%q:%q,%q:%q,%q:%q}\n"
:_time (os.date "!%FT%TZ")
:service "ERROR"
:_msg (string.format "can't parse log %q" msg)
:host hostname))
chunk)
))
(fn writefd [fd body]
(case (ll.write fd body)
(bytes) (when (< bytes (# body)) (writefd fd (string.sub body bytes)))
(nil errno)
(error (string.format "write to fd %d failed: %s" fd (ll.strerror errno))))
true)
(fn run []
(let [{ : auth : url } (parse-args arg)
in-fd 6
out-fd 7]
(writefd out-fd (http-header url.host url.path auth))
(while (case (io.stdin:read "l")
line (writefd out-fd (process-line line))))
(writefd out-fd (chunk ""))
(io.stderr:write (ll.read in-fd))))
{ : run }