@ 261101.7 · fetching

261026.37 · 21085a84dd5a7256f0a1f09b4f390e35ab0d43c6

Under systemd a line is logged once

--restart and failures log a line and print it on stderr. A unit's stderr is the journal, so each line showed twice there. Where JOURNAL_STREAM names stderr, www now only logs it.

reading declarations

log.go

@@ -164,6 +164,18 @@ func openLogFile(path string) (*os.File, error) { 	return os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o600) } +// stderrIsJournal reports whether stderr is a journal stream that systemd+// connected, as it does for a unit's process, by JOURNAL_STREAM, its+// device and inode. A line logged then needs no copy on stderr, which+// would show it twice in the journal.+func stderrIsJournal() bool {+	var st syscall.Stat_t+	if os.Getenv("JOURNAL_STREAM") == "" || syscall.Fstat(2, &st) != nil {+		return false+	}+	return os.Getenv("JOURNAL_STREAM") == fmt.Sprintf("%d:%d", st.Dev, st.Ino)+}+ // logSink is where a detached server's stderr goes: a journal stream, or // the fallback file. A panic writes there, past every logger. It returns // nil when neither opens, and stderr then goes nowhere, as before.

log_test.go

@@ -3,10 +3,12 @@ package main import ( 	"bytes" 	"encoding/binary"+	"fmt" 	"net" 	"os" 	"path/filepath" 	"strings"+	"syscall" 	"testing" 	"time" )@@ -175,3 +177,24 @@ func TestRequestLogShared(t *testing.T) { 		t.Errorf("shared log lacks the visitor:\n%s", b) 	} }++// Under systemd, stderr is the journal, so what is logged is not printed+// as well.+func TestStderrIsJournal(t *testing.T) {+	var st syscall.Stat_t+	if err := syscall.Fstat(2, &st); err != nil {+		t.Skip(err)+	}+	t.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d:%d", st.Dev, st.Ino))+	if !stderrIsJournal() {+		t.Error("stderr's own device and inode are not the journal")+	}+	t.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d:%d", st.Dev, st.Ino+1))+	if stderrIsJournal() {+		t.Error("another inode is the journal")+	}+	t.Setenv("JOURNAL_STREAM", "")+	if stderrIsJournal() {+		t.Error("no JOURNAL_STREAM is the journal")+	}+}

main.go

@@ -761,7 +761,9 @@ func detach(c config) int {  func failNow(code syscall.Errno, msg string) int { 	logErr("E%d: %s", code, msg)-	fmt.Fprintf(os.Stderr, "E%d: %s\n", code, msg)+	if !stderrIsJournal() {+		fmt.Fprintf(os.Stderr, "E%d: %s\n", code, msg)+	} 	return int(code) } 

restart.go

@@ -90,7 +90,9 @@ func reloading(n int) string { // say tells the user and the log what a restart did. func say(format string, args ...any) { 	logInfo(format, args...)-	fmt.Fprintf(os.Stderr, format+"\n", args...)+	if !stderrIsJournal() {+		fmt.Fprintf(os.Stderr, format+"\n", args...)+	} }  // startDir serves c.dir on c.ep once the servers are replaced. A tree the