Skip to content

Commit

Permalink
Merge pull request #410 from WGH-/env-journal-stream
Browse files Browse the repository at this point in the history
journal: add StderrIsJournalStream function
  • Loading branch information
lucab committed Nov 7, 2022
2 parents 43ee42e + 4ca6222 commit d5623bf
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 2 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/containers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
baseimage: ['debian:stretch', 'ubuntu:16.04', 'ubuntu:18.04']
baseimage: ['debian:stretch', 'ubuntu:18.04', 'ubuntu:20.04']
steps:
- run: sudo apt-get -qq update
- name: Install libsystemd-dev
Expand All @@ -38,7 +38,7 @@ jobs:
- name: Pull base image - ${{ matrix.baseimage }}
run: docker pull ${{ matrix.baseimage }}
- name: Install packages for ${{ matrix.baseimage }}
run: docker run --privileged -e GOPATH=${GOPATH} --cidfile=/tmp/cidfile ${{ matrix.baseimage }} /bin/bash -c "apt-get update && apt-get install -y sudo build-essential git golang dbus libsystemd-dev libpam-systemd systemd-container"
run: docker run --privileged -e GOPATH=${GOPATH} --cidfile=/tmp/cidfile ${{ matrix.baseimage }} /bin/bash -c "export DEBIAN_FRONTEND=noninteractive; apt-get update && apt-get install -y sudo build-essential git golang dbus libsystemd-dev libpam-systemd systemd-container"
- name: Persist base container
run: docker commit `cat /tmp/cidfile` go-systemd/container-tests
- run: rm -f /tmp/cidfile
Expand Down
37 changes: 37 additions & 0 deletions examples/journal/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2022 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 main

import (
"fmt"
"os"

"github.com/coreos/go-systemd/v22/journal"
)

func main() {
ok, err := journal.StderrIsJournalStream()
if err != nil {
panic(err)
}

if ok {
// use journal native protocol
journal.Send("this is a message logged through the native protocol", journal.PriInfo, nil)
} else {
// use stderr
fmt.Fprintln(os.Stderr, "this is a message logged through stderr")
}
}
13 changes: 13 additions & 0 deletions examples/journal/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash

set -e

go build

echo "Running directly"
./journal

echo "Running through systemd"
unit_name="run-$(systemd-id128 new)"
systemd-run -u "$unit_name" --user --wait --quiet ./journal
journalctl --user -u "$unit_name"
52 changes: 52 additions & 0 deletions journal/journal_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,58 @@ func Enabled() bool {
return true
}

// StderrIsJournalStream returns whether the process stderr is connected
// to the Journal's stream transport.
//
// This can be used for automatic protocol upgrading described in [Journal Native Protocol].
//
// Returns true if JOURNAL_STREAM environment variable is present,
// and stderr's device and inode numbers match it.
//
// Error is returned if unexpected error occurs: e.g. if JOURNAL_STREAM environment variable
// is present, but malformed, fstat syscall fails, etc.
//
// [Journal Native Protocol]: https://systemd.io/JOURNAL_NATIVE_PROTOCOL/#automatic-protocol-upgrading
func StderrIsJournalStream() (bool, error) {
return fdIsJournalStream(syscall.Stderr)
}

// StdoutIsJournalStream returns whether the process stdout is connected
// to the Journal's stream transport.
//
// Returns true if JOURNAL_STREAM environment variable is present,
// and stdout's device and inode numbers match it.
//
// Error is returned if unexpected error occurs: e.g. if JOURNAL_STREAM environment variable
// is present, but malformed, fstat syscall fails, etc.
//
// Most users should probably use [StderrIsJournalStream].
func StdoutIsJournalStream() (bool, error) {
return fdIsJournalStream(syscall.Stdout)
}

func fdIsJournalStream(fd int) (bool, error) {
journalStream := os.Getenv("JOURNAL_STREAM")
if journalStream == "" {
return false, nil
}

var expectedStat syscall.Stat_t
_, err := fmt.Sscanf(journalStream, "%d:%d", &expectedStat.Dev, &expectedStat.Ino)
if err != nil {
return false, fmt.Errorf("failed to parse JOURNAL_STREAM=%q: %v", journalStream, err)
}

var stat syscall.Stat_t
err = syscall.Fstat(fd, &stat)
if err != nil {
return false, err
}

match := stat.Dev == expectedStat.Dev && stat.Ino == expectedStat.Ino
return match, nil
}

// 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
Expand Down
188 changes: 188 additions & 0 deletions journal/journal_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright 2022 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.

//go:build !windows
// +build !windows

package journal_test

import (
"fmt"
"os"
"os/exec"
"syscall"
"testing"

"github.com/coreos/go-systemd/v22/journal"
)

func TestJournalStreamParsing(t *testing.T) {
if _, ok := os.LookupEnv("JOURNAL_STREAM"); ok {
t.Fatal("unset JOURNAL_STREAM before running this test")
}

t.Run("Missing", func(t *testing.T) {
ok, err := journal.StderrIsJournalStream()
if err != nil {
t.Fatal(err)
}
if ok {
t.Error("stderr shouldn't be connected to journal stream")
}
})
t.Run("Present", func(t *testing.T) {
f, stat := getUnixStreamSocket(t)
defer f.Close()
os.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d:%d", stat.Dev, stat.Ino))
defer os.Unsetenv("JOURNAL_STREAM")
replaceStderr(int(f.Fd()), func() {
ok, err := journal.StderrIsJournalStream()
if err != nil {
t.Fatal(err)
}
if !ok {
t.Error("stderr should've been connected to journal stream")
}
})
})
t.Run("NotMatching", func(t *testing.T) {
f, stat := getUnixStreamSocket(t)
defer f.Close()
os.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d:%d", stat.Dev+1, stat.Ino))
defer os.Unsetenv("JOURNAL_STREAM")
replaceStderr(int(f.Fd()), func() {
ok, err := journal.StderrIsJournalStream()
if err != nil {
t.Fatal(err)
}
if ok {
t.Error("stderr shouldn't be connected to journal stream")
}
})
})
t.Run("Malformed", func(t *testing.T) {
f, stat := getUnixStreamSocket(t)
defer f.Close()
os.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d-%d", stat.Dev, stat.Ino))
defer os.Unsetenv("JOURNAL_STREAM")
replaceStderr(int(f.Fd()), func() {
_, err := journal.StderrIsJournalStream()
if err == nil {
t.Fatal("JOURNAL_STREAM is malformed, but no error returned")
}
})
})
}

func TestStderrIsJournalStream(t *testing.T) {
const (
message = "TEST_MESSAGE"
)

userOrSystem := "--user"
if os.Getuid() == 0 {
userOrSystem = "--system"
}

if _, ok := os.LookupEnv("JOURNAL_STREAM"); !ok {
// Re-execute this test under systemd (see the else branch),
// and observe its exit code.
args := []string{
"systemd-run",
userOrSystem,
"--wait",
"--quiet",
"--",
os.Args[0],
"-test.run=TestStderrIsJournalStream",
"-test.count=1", // inhibit caching
}

cmd := exec.Command(args[0], args[1:]...)
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
t.Fatal(err)
}
} else {
ok, err := journal.StderrIsJournalStream()
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatal("StderrIsJournalStream should've returned true")
}

err = journal.Send(message, journal.PriInfo, nil)
if err != nil {
t.Fatal(err)
}
}
}

func ExampleStderrIsJournalStream() {
// NOTE: this is just an example. Production code
// will likely use this to setup a logging library
// to write messages to either journal or stderr.
ok, err := journal.StderrIsJournalStream()
if err != nil {
panic(err)
}

if ok {
// use journal native protocol
journal.Send("this is a message logged through the native protocol", journal.PriInfo, nil)
} else {
// use stderr
fmt.Fprintln(os.Stderr, "this is a message logged through stderr")
}
}

func replaceStderr(fd int, cb func()) {
savedStderr, err := syscall.Dup(syscall.Stderr)
if err != nil {
panic(err)
}
defer syscall.Close(savedStderr)
err = syscall.Dup2(fd, syscall.Stderr)
if err != nil {
panic(err)
}
defer func() {
err := syscall.Dup2(savedStderr, syscall.Stderr)
if err != nil {
panic(err)
}
}()
cb()
}

// getUnixStreamSocket returns a unix stream socket obtained with
// socketpair(2), and its fstat result. Only one end of the socket pair
// is returned, and the other end is closed immediately: we don't need
// it for our purposes.
func getUnixStreamSocket(t *testing.T) (*os.File, *syscall.Stat_t) {
fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
if err != nil {
t.Fatal(os.NewSyscallError("socketpair", err))
}
// we don't need the remote end for our tests
syscall.Close(fds[1])

file := os.NewFile(uintptr(fds[0]), "unix-stream")
stat, err := file.Stat()
if err != nil {
t.Fatal(err)
}
return file, stat.Sys().(*syscall.Stat_t)
}
8 changes: 8 additions & 0 deletions journal/journal_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,11 @@ func Enabled() bool {
func Send(message string, priority Priority, vars map[string]string) error {
return errors.New("could not initialize socket to journald")
}

func StderrIsJournalStream() (bool, error) {
return false, nil
}

func StdoutIsJournalStream() (bool, error) {
return false, nil
}
2 changes: 2 additions & 0 deletions scripts/ci-runner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ function build_tests {
echo " - examples/${ex}"
go build -o ./test_bins/${ex}.example ./examples/activation/${ex}.go
done
# just to make sure it's buildable
go build -o ./test_bins/journal ./examples/journal/
}

function run_tests {
Expand Down

0 comments on commit d5623bf

Please sign in to comment.