cmd/osbuild-composer: journald support

This commit is contained in:
Lukas Zapletal 2024-04-04 09:00:23 +02:00 committed by Achilleas Koutsou
parent e68449404f
commit f3c0daebbf
7 changed files with 345 additions and 3 deletions

View file

@ -129,7 +129,7 @@ func GetDefaultConfig() *ComposerConfigFile {
"rhel-10": "rhel-10.0",
},
LogLevel: "info",
LogFormat: "text",
LogFormat: "journal",
DNFJson: "/usr/libexec/osbuild-depsolve-dnf",
}
}

View file

@ -77,7 +77,7 @@ func TestDefaultConfig(t *testing.T) {
}
require.Equal(t, expectedDistroAliases, defaultConfig.DistroAliases)
require.Equal(t, "text", defaultConfig.LogFormat)
require.Equal(t, "journal", defaultConfig.LogFormat)
}
func TestConfig(t *testing.T) {

View file

@ -3,11 +3,15 @@ package main
import (
"context"
"flag"
"io"
"log"
"os"
"github.com/coreos/go-systemd/activation"
"github.com/coreos/go-systemd/journal"
"github.com/getsentry/sentry-go"
sentrylogrus "github.com/getsentry/sentry-go/logrus"
"github.com/osbuild/osbuild-composer/internal/common"
slogger "github.com/osbuild/osbuild-composer/pkg/splunk_logger"
"github.com/sirupsen/logrus"
)
@ -28,6 +32,11 @@ func main() {
flag.BoolVar(&verbose, "verbose", false, "Print access log")
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 {
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 {
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":
logrus.SetFormatter(&logrus.TextFormatter{})
case "json":

View 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,
}
}

View 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
View 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
View file

@ -436,7 +436,8 @@ github.com/coreos/go-semver/semver
# github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
## explicit
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
github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer
# github.com/cyphar/filepath-securejoin v0.2.4