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",
|
||||
},
|
||||
LogLevel: "info",
|
||||
LogFormat: "text",
|
||||
LogFormat: "journal",
|
||||
DNFJson: "/usr/libexec/osbuild-depsolve-dnf",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
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
|
||||
## 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue