From 1c3e7d43c178d188f673054991d4ce6d3ad39d62 Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 13:40:11 +0800 Subject: [PATCH 1/7] docs: package comments for every package --- sdk/go/silkd/conn.go | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/go/silkd/conn.go b/sdk/go/silkd/conn.go index 5ff9c63c..64fdc9bc 100644 --- a/sdk/go/silkd/conn.go +++ b/sdk/go/silkd/conn.go @@ -1,3 +1,4 @@ +// Package silkd frames a relay connection into silkd wire RPCs. package silkd import ( From d477b6e25b5524be277a0b74937daf4d78b30cea Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 13:42:34 +0800 Subject: [PATCH 2/7] lint: enable revive default rules Unused parameters become _ or unnamed, the max/delete parameters that shadow builtins become limit/deleteAfter, and the watch drain loops log each drained event instead of an empty body. --- .golangci.yml | 1 + sandboxd/egress/proxy_test.go | 2 +- sandboxd/engine/engine.go | 12 ++++++------ sandboxd/netfilter/nft_other.go | 2 +- sandboxd/pool/archive_test.go | 4 ++-- sandboxd/server/server_test.go | 8 ++++---- sdk/go/sandbox_test.go | 2 +- sdk/go/volumes_test.go | 4 ++-- sdk/go/watch_test.go | 6 ++++-- 9 files changed, 22 insertions(+), 19 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 80800e28..ed654591 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -38,6 +38,7 @@ linters: disable: - fieldalignment revive: + enable-default-rules: true rules: - name: exported disabled: true diff --git a/sandboxd/egress/proxy_test.go b/sandboxd/egress/proxy_test.go index f5e3c716..2f734828 100644 --- a/sandboxd/egress/proxy_test.go +++ b/sandboxd/egress/proxy_test.go @@ -118,7 +118,7 @@ func TestCloseReleasesIdleUpstreamConns(t *testing.T) { func TestForwardNeverInjectsInterceptSecret(t *testing.T) { reached := false - upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { reached = true })) defer upstream.Close() diff --git a/sandboxd/engine/engine.go b/sandboxd/engine/engine.go index 3696cbe1..27d4e1ec 100644 --- a/sandboxd/engine/engine.go +++ b/sandboxd/engine/engine.go @@ -448,22 +448,22 @@ func belowFloor(v string) (below, comparable bool) { return semver.Compare(v, RequiredCocoon) < 0, true } -func readBufLine(r *bufio.Reader, max int) (string, error) { +func readBufLine(r *bufio.Reader, limit int) (string, error) { line, err := r.ReadSlice('\n') if err != nil { return "", err } - if len(line) > max { - return "", fmt.Errorf("reply exceeds %d bytes", max) + if len(line) > limit { + return "", fmt.Errorf("reply exceeds %d bytes", limit) } return string(line[:len(line)-1]), nil } // readLine reads byte-wise so nothing past the newline is consumed. -func readLine(conn net.Conn, max int) (string, error) { +func readLine(conn net.Conn, limit int) (string, error) { var sb strings.Builder var b [1]byte - for sb.Len() < max { + for sb.Len() < limit { if _, err := io.ReadFull(conn, b[:]); err != nil { return "", err } @@ -472,7 +472,7 @@ func readLine(conn net.Conn, max int) (string, error) { } sb.WriteByte(b[0]) } - return "", fmt.Errorf("reply exceeds %d bytes", max) + return "", fmt.Errorf("reply exceeds %d bytes", limit) } func tail(s string) string { diff --git a/sandboxd/netfilter/nft_other.go b/sandboxd/netfilter/nft_other.go index 92c241e5..85ec622d 100644 --- a/sandboxd/netfilter/nft_other.go +++ b/sandboxd/netfilter/nft_other.go @@ -11,4 +11,4 @@ func Unlock(string) error { return errors.ErrUnsupported } func LockedTaps() (map[string]bool, error) { return nil, errors.ErrUnsupported } -func SweepExcept(keep map[string]bool) error { return nil } +func SweepExcept(map[string]bool) error { return nil } diff --git a/sandboxd/pool/archive_test.go b/sandboxd/pool/archive_test.go index 43b120cf..146d0290 100644 --- a/sandboxd/pool/archive_test.go +++ b/sandboxd/pool/archive_test.go @@ -932,13 +932,13 @@ func archivedCount(m *Manager) int { return g.Archived } -func archivePool(delete int) config.PoolSpec { +func archivePool(deleteAfter int) config.PoolSpec { return config.PoolSpec{ PoolKey: testKey, Warm: 1, IdleHibernateSeconds: 1, ArchiveAfterSeconds: 2, - ArchiveDeleteAfterSeconds: delete, + ArchiveDeleteAfterSeconds: deleteAfter, } } diff --git a/sandboxd/server/server_test.go b/sandboxd/server/server_test.go index 15b78d32..d49298b0 100644 --- a/sandboxd/server/server_test.go +++ b/sandboxd/server/server_test.go @@ -704,7 +704,7 @@ func TestAgentErrorPaths(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mgr := &fakeManager{socket: func(id, token string) (string, error) { return "/v/sock", tt.sockErr }} + mgr := &fakeManager{socket: func(string, string) (string, error) { return "/v/sock", tt.sockErr }} dialer := &fakeDialer{dial: func(context.Context, string) (net.Conn, error) { if tt.dialErr != nil { return nil, tt.dialErr @@ -737,7 +737,7 @@ func TestAgentErrorPaths(t *testing.T) { } func TestForkFlow(t *testing.T) { - mgr := &fakeManager{fork: func(id, token string, count int, ttl time.Duration) ([]*types.Sandbox, error) { + mgr := &fakeManager{fork: func(_, token string, count int, ttl time.Duration) ([]*types.Sandbox, error) { switch { case token != "tok": return nil, pool.ErrUnknownSandbox @@ -808,7 +808,7 @@ func TestPromoteAndDeleteTemplateFlow(t *testing.T) { var gotKey types.PoolKey mgr := &fakeManager{ promoteContentDigest: "sha256:promoted-digest", - promote: func(id, token, template string) error { + promote: func(_, token, template string) error { switch { case token != "tok": return pool.ErrUnknownSandbox @@ -1604,7 +1604,7 @@ func TestVolumeClaimRejectsShapeBeforePlacement(t *testing.T) { } func TestOwnerEndpoint(t *testing.T) { - mgr := &fakeManager{socket: func(id, token string) (string, error) { + mgr := &fakeManager{socket: func(_, token string) (string, error) { if token == "good" { return "/v/sock", nil } diff --git a/sdk/go/sandbox_test.go b/sdk/go/sandbox_test.go index 2e457ef9..da86bc2b 100644 --- a/sdk/go/sandbox_test.go +++ b/sdk/go/sandbox_test.go @@ -124,7 +124,7 @@ func TestRunCancelKillsTheCommandItStarted(t *testing.T) { func TestUpgradeKeepsCoalescedBytes(t *testing.T) { mux := http.NewServeMux() - mux.HandleFunc("GET /v1/sandboxes/{id}/agent", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("GET /v1/sandboxes/{id}/agent", func(w http.ResponseWriter, _ *http.Request) { conn, _, err := http.NewResponseController(w).Hijack() if err != nil { t.Errorf("hijack: %v", err) diff --git a/sdk/go/volumes_test.go b/sdk/go/volumes_test.go index 301741f1..2c102dc9 100644 --- a/sdk/go/volumes_test.go +++ b/sdk/go/volumes_test.go @@ -260,7 +260,7 @@ func TestWithVolumesAttachOnlySendsFlagAndDecodesMountlessEcho(t *testing.T) { } func TestWithVolumesAttachOnlyRejectsMountLocally(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + ts := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { t.Error("server should not be contacted for a locally invalid claim") })) t.Cleanup(ts.Close) @@ -287,7 +287,7 @@ func TestRejectsInvalidVolumeModeLocally(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + ts := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { t.Error("server should not be contacted for a locally invalid mode") })) t.Cleanup(ts.Close) diff --git a/sdk/go/watch_test.go b/sdk/go/watch_test.go index 957be6d9..3a1e53a9 100644 --- a/sdk/go/watch_test.go +++ b/sdk/go/watch_test.go @@ -34,7 +34,8 @@ func TestWatchDeliversEventsUntilClose(t *testing.T) { if err := w.Close(); err != nil { t.Fatalf("Close: %v", err) } - for range w.Events() { + for ev := range w.Events() { + t.Logf("drained %+v", ev) } if err := w.Err(); err != nil { t.Errorf("Err after clean close: %v", err) @@ -61,7 +62,8 @@ func TestWatchReleasesTheRelayWhenTheSandboxEnds(t *testing.T) { if err != nil { t.Fatalf("Watch: %v", err) } - for range w.Events() { + for ev := range w.Events() { + t.Logf("drained %+v", ev) } if e, ok := errors.AsType[*wire.ErrorResp](w.Err()); !ok || e.Message != "watcher died" { t.Fatalf("Err = %v, want the sandbox's error frame", w.Err()) From 4de77f3839f06964c3f23ec0aeea90b144fb93bf Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 13:44:48 +0800 Subject: [PATCH 3/7] lint: enable nolintlint, drop unused directives Every existing directive is still used; the config change is the whole diff. --- .golangci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index ed654591..57085644 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -24,6 +24,7 @@ linters: - modernize - nakedret - nestif + - nolintlint - revive - staticcheck - unconvert @@ -42,6 +43,8 @@ linters: rules: - name: exported disabled: true + nolintlint: + allow-unused: false misspell: locale: US goconst: From 78e380684c9e6fd87c876603c992e245ebeb9ff0 Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 13:45:10 +0800 Subject: [PATCH 4/7] build: go 1.27.1 --- e2e/go.mod | 2 +- mcp/go.mod | 2 +- protocol/wire/go.mod | 2 +- sandboxd/go.mod | 2 +- sdk/go/go.mod | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/e2e/go.mod b/e2e/go.mod index f2eb5617..6207fa90 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -1,6 +1,6 @@ module github.com/cocoonstack/sandbox/e2e -go 1.27.0 +go 1.27.1 require ( github.com/aws/aws-sdk-go-v2 v1.47.0 // indirect diff --git a/mcp/go.mod b/mcp/go.mod index fb2ec83e..5f3ae24c 100644 --- a/mcp/go.mod +++ b/mcp/go.mod @@ -1,6 +1,6 @@ module github.com/cocoonstack/sandbox/mcp -go 1.27.0 +go 1.27.1 require ( github.com/cocoonstack/sandbox/protocol/wire v0.1.13 diff --git a/protocol/wire/go.mod b/protocol/wire/go.mod index 92c56333..f917207d 100644 --- a/protocol/wire/go.mod +++ b/protocol/wire/go.mod @@ -1,3 +1,3 @@ module github.com/cocoonstack/sandbox/protocol/wire -go 1.27.0 +go 1.27.1 diff --git a/sandboxd/go.mod b/sandboxd/go.mod index 48c3ed6a..6ea36442 100644 --- a/sandboxd/go.mod +++ b/sandboxd/go.mod @@ -1,6 +1,6 @@ module github.com/cocoonstack/sandbox/sandboxd -go 1.27.0 +go 1.27.1 require ( github.com/aws/aws-sdk-go-v2 v1.47.0 diff --git a/sdk/go/go.mod b/sdk/go/go.mod index efff971b..cff289a9 100644 --- a/sdk/go/go.mod +++ b/sdk/go/go.mod @@ -1,6 +1,6 @@ module github.com/cocoonstack/sandbox/sdk/go -go 1.27.0 +go 1.27.1 replace github.com/cocoonstack/sandbox/protocol/wire => ../../protocol/wire From 49bac549c1040185af1f391e2394175272ee23cd Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 13:54:44 +0800 Subject: [PATCH 5/7] build: rust 1.98.1 Move the boot/init and silkd toolchain channel, rust-version and builder base images together; supersedes the image-only dependabot bumps. --- boot/Dockerfile | 2 +- boot/init/Cargo.toml | 2 +- boot/init/rust-toolchain.toml | 2 +- silkd/Cargo.toml | 2 +- silkd/Dockerfile | 2 +- silkd/rust-toolchain.toml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/boot/Dockerfile b/boot/Dockerfile index 559e4b69..573623bc 100644 --- a/boot/Dockerfile +++ b/boot/Dockerfile @@ -75,7 +75,7 @@ RUN set -e; \ fi; \ gcc usr/gen_init_cpio.c -o /usr/local/bin/gen_init_cpio -FROM rust:1.98.0-alpine@sha256:a10e64dd139b7387337c7fbe8aca31b959b57b2fd4c8ae20a02cf1d6ea424dce AS initbuild +FROM rust:1.98.1-alpine@sha256:7cc1c22d77d9432f7fe012a70e6d3e555af54c2a6832700ed7d553f1769ae89f AS initbuild RUN apk add --no-cache musl-dev WORKDIR /build # See silkd/Dockerfile: the pin, not the base image's default, is the compiler. diff --git a/boot/init/Cargo.toml b/boot/init/Cargo.toml index 872c63c9..928bab14 100644 --- a/boot/init/Cargo.toml +++ b/boot/init/Cargo.toml @@ -2,7 +2,7 @@ name = "sandbox-init" version = "0.1.0" edition = "2024" -rust-version = "1.98" +rust-version = "1.98.1" description = "Initramfs PID 1: assemble the EROFS+overlay rootfs and switch_root" publish = false diff --git a/boot/init/rust-toolchain.toml b/boot/init/rust-toolchain.toml index b36f60f5..c9901407 100644 --- a/boot/init/rust-toolchain.toml +++ b/boot/init/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.98.0" +channel = "1.98.1" components = ["rustfmt", "clippy"] profile = "minimal" diff --git a/silkd/Cargo.toml b/silkd/Cargo.toml index d66903d8..01b6edc1 100644 --- a/silkd/Cargo.toml +++ b/silkd/Cargo.toml @@ -2,7 +2,7 @@ name = "silkd" version = "0.1.13" edition = "2024" -rust-version = "1.98" +rust-version = "1.98.1" description = "In-guest sandbox daemon: exec with context, processes, and files over vsock" publish = false diff --git a/silkd/Dockerfile b/silkd/Dockerfile index 855e6111..b1843dca 100644 --- a/silkd/Dockerfile +++ b/silkd/Dockerfile @@ -7,7 +7,7 @@ # /silkd-static is musl-static for guests without glibc, such as the android # flavor's non-systemd userspace. Built per-platform (amd64/arm64) on native # runners; TARGETARCH picks the musl target triple. -FROM rust:1.98.0-slim@sha256:17d1ba895198f9934c6314ec5346a0d5115372f3243390c3d731e242f35c2f27 AS build +FROM rust:1.98.1-slim@sha256:f47a8de237dcbb0b0ce1099901e60a89728e3d51f24e664b40e947171538ade7 AS build ARG TARGETARCH WORKDIR /silkd # Before any rustup/cargo call: the pin, not the base image's default, is the diff --git a/silkd/rust-toolchain.toml b/silkd/rust-toolchain.toml index 34c2f40e..753d3947 100644 --- a/silkd/rust-toolchain.toml +++ b/silkd/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.98.0" +channel = "1.98.1" components = ["rustfmt", "clippy"] targets = ["x86_64-unknown-linux-musl"] profile = "minimal" From 5207b0f9aa3cff10208a3944bb05f4ddc6d90f64 Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 13:54:44 +0800 Subject: [PATCH 6/7] ci: image builds skip Markdown-only changes A README edit under protocol/, silkd/ or os-image/ started the image chain; a protocol or silkd README rebuilt the silkd carrier and refolded it into every image. The images.yml push filter now drops *.md, and both change detections (images.yml detect, the build-os-images matrix) ignore it, so a mixed push does not rebuild an image for its doc half; the matrix no-cache loop's own *.md skip is redundant and goes. Simulated on real history (first-parent commits, before = parent): f6477cf and 401c0aa (protocol/silkd README only) go from a silkd carrier, base and three flavors to no run; b0283fc (android README only) goes from an android build to no run; e1d5973 (desktop Dockerfile + README) keeps its one cached desktop build; 4c56260 (node README + silkd Cargo.lock) keeps its full silkd refold. --- .github/workflows/build-os-images.yml | 4 ++-- .github/workflows/images.yml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-os-images.yml b/.github/workflows/build-os-images.yml index de9f82c0..e68ba251 100644 --- a/.github/workflows/build-os-images.yml +++ b/.github/workflows/build-os-images.yml @@ -85,7 +85,7 @@ jobs: all_names all_changed="" else - all_changed=$(git diff --name-only "$BEFORE" "$SHA" -- 'os-image/' | sort -u) + all_changed=$(git diff --name-only "$BEFORE" "$SHA" -- 'os-image/' ':!*.md' | sort -u) declare -A names for f in $all_changed; do name=$(echo "$f" | cut -d'/' -f2) @@ -112,7 +112,7 @@ jobs: needs_no_cache="true" elif [[ -n "$all_changed" ]]; then while IFS= read -r f; do - [[ -z "$f" || "$f" == *.md ]] && continue + [[ -z "$f" ]] && continue depth=$(echo "$f" | awk -F'/' '{print NF}') base_name=$(basename "$f") if [[ "$depth" -eq 3 ]] || { [[ "$depth" -eq 4 ]] && [[ "$base_name" != "Dockerfile" ]]; }; then diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index 312bc82a..cb465df7 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -19,6 +19,7 @@ on: - ".github/workflows/build-os-images.yml" - ".github/workflows/build-silkd.yml" - ".github/workflows/build-boot.yml" + - "!**/*.md" workflow_dispatch: inputs: no-cache: @@ -58,7 +59,7 @@ jobs: if [[ "$EVT" == "workflow_dispatch" || -z "$BEFORE" || "$BEFORE" == "0000000000000000000000000000000000000000" ]]; then boot=true; silkd=true; osimage=true else - changed() { git diff --name-only "$BEFORE" "$SHA" -- "$@" | grep -q .; } + changed() { git diff --name-only "$BEFORE" "$SHA" -- "$@" ':!*.md' | grep -q .; } # Each area carries its own build workflow: changing how an image is # built must rebuild it, or the run goes green having built nothing. wf=.github/workflows From 590b21967ee4637dd67a8a101cf6711b73e2de3f Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 15:03:43 +0800 Subject: [PATCH 7/7] log: JSON records when stderr is not a terminal sandbox-mcp sets UseJSON from a stderr terminal check. sandboxd replaces the core/log writer with its journald writer, so UseJSON on SetupLog would be dead there; the journal writer instead emits the raw JSON record behind the syslog prefix, in one write, when stderr is not a terminal, keeping journald's priority classification, and a terminal keeps the console line. Hot path: the JSON path skips the per-record console re-render. --- docs/deploy.md | 8 +++++--- mcp/main.go | 7 ++++++- sandboxd/log.go | 22 ++++++++++++++++------ sandboxd/log_test.go | 13 ++++++++++++- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/docs/deploy.md b/docs/deploy.md index d3d1400d..ef5a47cb 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -439,9 +439,11 @@ WantedBy=multi-user.target Each record carries its syslog level, so alerting keys on systemd priority: `journalctl -u sandboxd -p err` lists the errors and `-p warning` the -degradations. `journalctl` answers an empty range with `-- No entries --`, so -a check that counts lines reads one problem where there are none — count -records (`-o json | wc -l`) or test the output for emptiness. +degradations. With stderr off a terminal, as under this unit, the record +itself is one JSON object; a terminal gets the console format. `journalctl` +answers an empty range with `-- No entries --`, so a check that counts lines +reads one problem where there are none — count records (`-o json | wc -l`) or +test the output for emptiness. Stopping sandboxd leaves VMs alive; the next start reconciles them. The unit's `KillMode=process` is what guarantees it: systemd signals sandboxd alone, so a diff --git a/mcp/main.go b/mcp/main.go index 73a68afb..7ee4bcf0 100644 --- a/mcp/main.go +++ b/mcp/main.go @@ -24,7 +24,7 @@ const shutdownGrace = 10 * time.Second func main() { ctx := context.Background() - if err := log.SetupLog(ctx, &coretypes.ServerLogConfig{Level: "error"}, ""); err != nil { + if err := log.SetupLog(ctx, &coretypes.ServerLogConfig{Level: "error", UseJSON: !stderrIsTerminal()}, ""); err != nil { fmt.Fprintln(os.Stderr, "setup log:", err) os.Exit(1) } @@ -56,3 +56,8 @@ func main() { } } } + +func stderrIsTerminal() bool { + fi, err := os.Stderr.Stat() + return err == nil && fi.Mode()&os.ModeCharDevice != 0 +} diff --git a/sandboxd/log.go b/sandboxd/log.go index 11335344..5c3c47c4 100644 --- a/sandboxd/log.go +++ b/sandboxd/log.go @@ -26,6 +26,7 @@ var _ zerolog.LevelWriter = journalWriter{} type journalWriter struct { console zerolog.ConsoleWriter out io.Writer + json bool } func (j journalWriter) Write(p []byte) (int, error) { return j.WriteLevel(zerolog.NoLevel, p) } @@ -33,9 +34,13 @@ func (j journalWriter) Write(p []byte) (int, error) { return j.WriteLevel(zerolo func (j journalWriter) WriteLevel(level zerolog.Level, p []byte) (int, error) { buf := &bytes.Buffer{} buf.WriteString(syslogPrefix(level)) - j.console.Out = buf - if _, err := j.console.Write(p); err != nil { - return 0, err + if j.json { + buf.Write(p) + } else { + j.console.Out = buf + if _, err := j.console.Write(p); err != nil { + return 0, err + } } if _, err := j.out.Write(buf.Bytes()); err != nil { return 0, err @@ -48,12 +53,12 @@ func setupLog(ctx context.Context, level string) error { return err } logger := log.GetGlobalLogger() - *logger = zerolog.New(newJournalWriter(os.Stderr)).With().Timestamp().Logger().Level(logger.GetLevel()) + *logger = zerolog.New(newJournalWriter(os.Stderr, !stderrIsTerminal())).With().Timestamp().Logger().Level(logger.GetLevel()) return nil } -func newJournalWriter(out io.Writer) journalWriter { - return journalWriter{console: zerolog.ConsoleWriter{TimeFormat: time.RFC822, NoColor: true}, out: out} +func newJournalWriter(out io.Writer, json bool) journalWriter { + return journalWriter{console: zerolog.ConsoleWriter{TimeFormat: time.RFC822, NoColor: true}, out: out, json: json} } func syslogPrefix(level zerolog.Level) string { @@ -70,3 +75,8 @@ func syslogPrefix(level zerolog.Level) string { return syslogInfo } } + +func stderrIsTerminal() bool { + fi, err := os.Stderr.Stat() + return err == nil && fi.Mode()&os.ModeCharDevice != 0 +} diff --git a/sandboxd/log_test.go b/sandboxd/log_test.go index 879116e2..77054e6c 100644 --- a/sandboxd/log_test.go +++ b/sandboxd/log_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "encoding/json" "io" "strings" "testing" @@ -61,8 +62,18 @@ func TestSyslogPrefixMapsFatalToCritical(t *testing.T) { } } +func TestJournalWriterKeepsJSONRecordsBehindThePrefix(t *testing.T) { + counter := &countingWriter{} + logger := zerolog.New(newJournalWriter(counter, true)) + logger.Error().Str("k", "v").Msg("boom") + line := counter.buf.String() + if counter.writes != 1 || !strings.HasPrefix(line, syslogErr+"{") || !json.Valid([]byte(line[len(syslogErr):])) { + t.Errorf("writes = %d, line = %q, want one write of %q and one JSON record", counter.writes, line, syslogErr) + } +} + func testLogger(out io.Writer) zerolog.Logger { - return zerolog.New(newJournalWriter(out)) + return zerolog.New(newJournalWriter(out, false)) } type countingWriter struct {