cmd/osbuild-composer: journald support
This commit is contained in:
parent
e68449404f
commit
f3c0daebbf
7 changed files with 345 additions and 3 deletions
|
|
@ -129,7 +129,7 @@ func GetDefaultConfig() *ComposerConfigFile {
|
||||||
"rhel-10": "rhel-10.0",
|
"rhel-10": "rhel-10.0",
|
||||||
},
|
},
|
||||||
LogLevel: "info",
|
LogLevel: "info",
|
||||||
LogFormat: "text",
|
LogFormat: "journal",
|
||||||
DNFJson: "/usr/libexec/osbuild-depsolve-dnf",
|
DNFJson: "/usr/libexec/osbuild-depsolve-dnf",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ func TestDefaultConfig(t *testing.T) {
|
||||||
}
|
}
|
||||||
require.Equal(t, expectedDistroAliases, defaultConfig.DistroAliases)
|
require.Equal(t, expectedDistroAliases, defaultConfig.DistroAliases)
|
||||||
|
|
||||||
require.Equal(t, "text", defaultConfig.LogFormat)
|
require.Equal(t, "journal", defaultConfig.LogFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig(t *testing.T) {
|
func TestConfig(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,15 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/coreos/go-systemd/activation"
|
"github.com/coreos/go-systemd/activation"
|
||||||
|
"github.com/coreos/go-systemd/journal"
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
sentrylogrus "github.com/getsentry/sentry-go/logrus"
|
sentrylogrus "github.com/getsentry/sentry-go/logrus"
|
||||||
|
"github.com/osbuild/osbuild-composer/internal/common"
|
||||||
slogger "github.com/osbuild/osbuild-composer/pkg/splunk_logger"
|
slogger "github.com/osbuild/osbuild-composer/pkg/splunk_logger"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
@ -28,6 +32,11 @@ func main() {
|
||||||
flag.BoolVar(&verbose, "verbose", false, "Print access log")
|
flag.BoolVar(&verbose, "verbose", false, "Print access log")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
// Redirect Go standard logger into logrus before it's used by other packages
|
||||||
|
log.SetOutput(common.Logger())
|
||||||
|
// Ensure the Go standard logger does not have any prefix or timestamp
|
||||||
|
log.SetFlags(0)
|
||||||
|
|
||||||
if !verbose {
|
if !verbose {
|
||||||
logrus.Print("verbose flag is provided for backward compatibility only, current behavior is always printing the access log")
|
logrus.Print("verbose flag is provided for backward compatibility only, current behavior is always printing the access log")
|
||||||
}
|
}
|
||||||
|
|
@ -49,6 +58,16 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch config.LogFormat {
|
switch config.LogFormat {
|
||||||
|
case "journal":
|
||||||
|
// If we are running under systemd, use the journal. Otherwise,
|
||||||
|
// fallback to text formatter.
|
||||||
|
if journal.Enabled() {
|
||||||
|
logrus.SetFormatter(&logrus.JSONFormatter{})
|
||||||
|
logrus.AddHook(&common.JournalHook{})
|
||||||
|
logrus.SetOutput(io.Discard)
|
||||||
|
} else {
|
||||||
|
logrus.SetFormatter(&logrus.TextFormatter{})
|
||||||
|
}
|
||||||
case "text":
|
case "text":
|
||||||
logrus.SetFormatter(&logrus.TextFormatter{})
|
logrus.SetFormatter(&logrus.TextFormatter{})
|
||||||
case "json":
|
case "json":
|
||||||
|
|
|
||||||
70
internal/common/journal_hook.go
Normal file
70
internal/common/journal_hook.go
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
// Inspired by github.com/wercker/journalhook (MIT license)
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/go-systemd/journal"
|
||||||
|
logrus "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JournalHook struct{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
severityMap = map[logrus.Level]journal.Priority{
|
||||||
|
logrus.DebugLevel: journal.PriDebug,
|
||||||
|
logrus.InfoLevel: journal.PriInfo,
|
||||||
|
logrus.WarnLevel: journal.PriWarning,
|
||||||
|
logrus.ErrorLevel: journal.PriErr,
|
||||||
|
logrus.FatalLevel: journal.PriCrit,
|
||||||
|
logrus.PanicLevel: journal.PriEmerg,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func stringifyOp(r rune) rune {
|
||||||
|
switch {
|
||||||
|
case r >= 'A' && r <= 'Z':
|
||||||
|
return r
|
||||||
|
case r >= '0' && r <= '9':
|
||||||
|
return r
|
||||||
|
case r == '_':
|
||||||
|
return r
|
||||||
|
case r >= 'a' && r <= 'z':
|
||||||
|
return r - 32
|
||||||
|
default:
|
||||||
|
return rune('_')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifyKey(key string) string {
|
||||||
|
key = strings.Map(stringifyOp, key)
|
||||||
|
key = strings.TrimPrefix(key, "_")
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Journal wants strings but logrus takes anything.
|
||||||
|
func stringifyEntries(data map[string]interface{}) map[string]string {
|
||||||
|
entries := make(map[string]string)
|
||||||
|
for k, v := range data {
|
||||||
|
|
||||||
|
key := stringifyKey(k)
|
||||||
|
entries[key] = fmt.Sprint(v)
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hook *JournalHook) Fire(entry *logrus.Entry) error {
|
||||||
|
return journal.Send(entry.Message, severityMap[entry.Level], stringifyEntries(entry.Data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hook *JournalHook) Levels() []logrus.Level {
|
||||||
|
return []logrus.Level{
|
||||||
|
logrus.PanicLevel,
|
||||||
|
logrus.FatalLevel,
|
||||||
|
logrus.ErrorLevel,
|
||||||
|
logrus.WarnLevel,
|
||||||
|
logrus.InfoLevel,
|
||||||
|
logrus.DebugLevel,
|
||||||
|
}
|
||||||
|
}
|
||||||
27
internal/common/journal_hook_test.go
Normal file
27
internal/common/journal_hook_test.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestStringifyEntries(t *testing.T) {
|
||||||
|
input := map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": 123,
|
||||||
|
"foo-foo": "x",
|
||||||
|
"-bar": "1",
|
||||||
|
}
|
||||||
|
|
||||||
|
output := stringifyEntries(input)
|
||||||
|
if output["FOO"] != "bar" {
|
||||||
|
t.Fatalf("%v", output)
|
||||||
|
t.Fatalf("expected value 'bar'. Got %q", output["FOO"])
|
||||||
|
}
|
||||||
|
if output["BAZ"] != "123" {
|
||||||
|
t.Fatalf("expected value '123'. Got %q", output["BAZ"])
|
||||||
|
}
|
||||||
|
if output["FOO_FOO"] != "x" {
|
||||||
|
t.Fatalf("expected value 'x'. Got %q", output["FOO_FOO"])
|
||||||
|
}
|
||||||
|
if output["BAR"] != "1" {
|
||||||
|
t.Fatalf("expected value 'x'. Got %q", output["BAR"])
|
||||||
|
}
|
||||||
|
}
|
||||||
225
vendor/github.com/coreos/go-systemd/journal/journal.go
generated
vendored
Normal file
225
vendor/github.com/coreos/go-systemd/journal/journal.go
generated
vendored
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
// Copyright 2015 CoreOS, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package journal provides write bindings to the local systemd journal.
|
||||||
|
// It is implemented in pure Go and connects to the journal directly over its
|
||||||
|
// unix socket.
|
||||||
|
//
|
||||||
|
// To read from the journal, see the "sdjournal" package, which wraps the
|
||||||
|
// sd-journal a C API.
|
||||||
|
//
|
||||||
|
// http://www.freedesktop.org/software/systemd/man/systemd-journald.service.html
|
||||||
|
package journal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Priority of a journal message
|
||||||
|
type Priority int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PriEmerg Priority = iota
|
||||||
|
PriAlert
|
||||||
|
PriCrit
|
||||||
|
PriErr
|
||||||
|
PriWarning
|
||||||
|
PriNotice
|
||||||
|
PriInfo
|
||||||
|
PriDebug
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// This can be overridden at build-time:
|
||||||
|
// https://github.com/golang/go/wiki/GcToolchainTricks#including-build-information-in-the-executable
|
||||||
|
journalSocket = "/run/systemd/journal/socket"
|
||||||
|
|
||||||
|
// unixConnPtr atomically holds the local unconnected Unix-domain socket.
|
||||||
|
// Concrete safe pointer type: *net.UnixConn
|
||||||
|
unixConnPtr unsafe.Pointer
|
||||||
|
// onceConn ensures that unixConnPtr is initialized exactly once.
|
||||||
|
onceConn sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
onceConn.Do(initConn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enabled checks whether the local systemd journal is available for logging.
|
||||||
|
func Enabled() bool {
|
||||||
|
onceConn.Do(initConn)
|
||||||
|
|
||||||
|
if (*net.UnixConn)(atomic.LoadPointer(&unixConnPtr)) == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := net.Dial("unixgram", journalSocket); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a message to the local systemd journal. vars is a map of journald
|
||||||
|
// fields to values. Fields must be composed of uppercase letters, numbers,
|
||||||
|
// and underscores, but must not start with an underscore. Within these
|
||||||
|
// restrictions, any arbitrary field name may be used. Some names have special
|
||||||
|
// significance: see the journalctl documentation
|
||||||
|
// (http://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html)
|
||||||
|
// for more details. vars may be nil.
|
||||||
|
func Send(message string, priority Priority, vars map[string]string) error {
|
||||||
|
conn := (*net.UnixConn)(atomic.LoadPointer(&unixConnPtr))
|
||||||
|
if conn == nil {
|
||||||
|
return errors.New("could not initialize socket to journald")
|
||||||
|
}
|
||||||
|
|
||||||
|
socketAddr := &net.UnixAddr{
|
||||||
|
Name: journalSocket,
|
||||||
|
Net: "unixgram",
|
||||||
|
}
|
||||||
|
|
||||||
|
data := new(bytes.Buffer)
|
||||||
|
appendVariable(data, "PRIORITY", strconv.Itoa(int(priority)))
|
||||||
|
appendVariable(data, "MESSAGE", message)
|
||||||
|
for k, v := range vars {
|
||||||
|
appendVariable(data, k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := conn.WriteMsgUnix(data.Bytes(), nil, socketAddr)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !isSocketSpaceError(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Large log entry, send it via tempfile and ancillary-fd.
|
||||||
|
file, err := tempFd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
_, err = io.Copy(file, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rights := syscall.UnixRights(int(file.Fd()))
|
||||||
|
_, _, err = conn.WriteMsgUnix([]byte{}, rights, socketAddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print prints a message to the local systemd journal using Send().
|
||||||
|
func Print(priority Priority, format string, a ...interface{}) error {
|
||||||
|
return Send(fmt.Sprintf(format, a...), priority, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendVariable(w io.Writer, name, value string) {
|
||||||
|
if err := validVarName(name); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "variable name %s contains invalid character, ignoring\n", name)
|
||||||
|
}
|
||||||
|
if strings.ContainsRune(value, '\n') {
|
||||||
|
/* When the value contains a newline, we write:
|
||||||
|
* - the variable name, followed by a newline
|
||||||
|
* - the size (in 64bit little endian format)
|
||||||
|
* - the data, followed by a newline
|
||||||
|
*/
|
||||||
|
fmt.Fprintln(w, name)
|
||||||
|
binary.Write(w, binary.LittleEndian, uint64(len(value)))
|
||||||
|
fmt.Fprintln(w, value)
|
||||||
|
} else {
|
||||||
|
/* just write the variable and value all on one line */
|
||||||
|
fmt.Fprintf(w, "%s=%s\n", name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validVarName validates a variable name to make sure journald will accept it.
|
||||||
|
// The variable name must be in uppercase and consist only of characters,
|
||||||
|
// numbers and underscores, and may not begin with an underscore:
|
||||||
|
// https://www.freedesktop.org/software/systemd/man/sd_journal_print.html
|
||||||
|
func validVarName(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return errors.New("Empty variable name")
|
||||||
|
} else if name[0] == '_' {
|
||||||
|
return errors.New("Variable name begins with an underscore")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range name {
|
||||||
|
if !(('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || c == '_') {
|
||||||
|
return errors.New("Variable name contains invalid characters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSocketSpaceError checks whether the error is signaling
|
||||||
|
// an "overlarge message" condition.
|
||||||
|
func isSocketSpaceError(err error) bool {
|
||||||
|
opErr, ok := err.(*net.OpError)
|
||||||
|
if !ok || opErr == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
sysErr, ok := opErr.Err.(*os.SyscallError)
|
||||||
|
if !ok || sysErr == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return sysErr.Err == syscall.EMSGSIZE || sysErr.Err == syscall.ENOBUFS
|
||||||
|
}
|
||||||
|
|
||||||
|
// tempFd creates a temporary, unlinked file under `/dev/shm`.
|
||||||
|
func tempFd() (*os.File, error) {
|
||||||
|
file, err := ioutil.TempFile("/dev/shm/", "journal.XXXXX")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = syscall.Unlink(file.Name())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initConn initializes the global `unixConnPtr` socket.
|
||||||
|
// It is meant to be called exactly once, at program startup.
|
||||||
|
func initConn() {
|
||||||
|
autobind, err := net.ResolveUnixAddr("unixgram", "")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sock, err := net.ListenUnixgram("unixgram", autobind)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.StorePointer(&unixConnPtr, unsafe.Pointer(sock))
|
||||||
|
}
|
||||||
3
vendor/modules.txt
vendored
3
vendor/modules.txt
vendored
|
|
@ -436,7 +436,8 @@ github.com/coreos/go-semver/semver
|
||||||
# github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
# github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||||
## explicit
|
## explicit
|
||||||
github.com/coreos/go-systemd/activation
|
github.com/coreos/go-systemd/activation
|
||||||
# github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f
|
github.com/coreos/go-systemd/journal
|
||||||
|
# github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46
|
||||||
## explicit
|
## explicit
|
||||||
github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer
|
github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer
|
||||||
# github.com/cyphar/filepath-securejoin v0.2.4
|
# github.com/cyphar/filepath-securejoin v0.2.4
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue