Skip to content

Commit ea6e5b6

Browse files
Andrew-Lees11GitHub Enterprise
authored andcommitted
Prevent races when writing to stdout/stderr (#1032)
1 parent 54eb45b commit ea6e5b6

File tree

7 files changed

+93
-24
lines changed

7 files changed

+93
-24
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
# Change log
2+
3+
## 9.4.0.15-r2 (2025-09)
4+
5+
* Logs now consistently routed to stdout/stderr; logs to stdout will conform to the selected log format (MQ_LOGGING_CONSOLE_FORMAT), logs to stderr are unformatted errors
6+
27
## 9.4.0.15-r1 (2025-08)
38

49
* Updated to MQ version 9.4.0.15

cmd/runmqdevserver/main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,17 @@ func configureLogger() error {
6565
}
6666
switch f {
6767
case "json":
68-
log, err = logger.NewLogger(os.Stderr, d, true, n)
68+
log, err = logger.NewLogger(os.Stdout, d, true, n)
6969
if err != nil {
7070
return err
7171
}
7272
case "basic":
73-
log, err = logger.NewLogger(os.Stderr, d, false, n)
73+
log, err = logger.NewLogger(os.Stdout, d, false, n)
7474
if err != nil {
7575
return err
7676
}
7777
default:
78-
log, err = logger.NewLogger(os.Stdout, d, false, n)
78+
log, err = logger.NewLogger(os.Stderr, d, false, n)
7979
return fmt.Errorf("invalid value for LOG_FORMAT: %v", f)
8080
}
8181
return nil

cmd/runmqserver/logging.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/ibm-messaging/mq-container/internal/command"
3232
"github.com/ibm-messaging/mq-container/pkg/logger"
3333
"github.com/ibm-messaging/mq-container/pkg/mqini"
34+
"github.com/ibm-messaging/mq-container/pkg/syncwriter"
3435
)
3536

3637
// var debug = false
@@ -241,10 +242,11 @@ func configureLogger(name string) (mirrorFunc, error) {
241242
d := getDebug()
242243
switch f {
243244
case "json":
244-
log, err = logger.NewLogger(os.Stderr, d, true, name)
245+
log, err = logger.NewLogger(os.Stdout, d, true, name)
245246
if err != nil {
246247
return nil, err
247248
}
249+
mirrorLogWriter := syncwriter.For(os.Stdout)
248250
return func(msg string, isQMLog bool) bool {
249251
arrLoggingConsoleExcludeIds := strings.Split(strings.ToUpper(os.Getenv("MQ_LOGGING_CONSOLE_EXCLUDE_ID")), ",")
250252
if isExcludedMsgIdPresent(msg, arrLoggingConsoleExcludeIds) {
@@ -260,24 +262,25 @@ func configureLogger(name string) (mirrorFunc, error) {
260262
if err != nil {
261263
log.Printf("Failed to unmarshall JSON in log message - %v", msg)
262264
} else {
263-
fmt.Println(msg)
265+
mirrorLogWriter.Println(msg)
264266
}
265267
} else {
266268
// The log being mirrored isn't JSON. This can happen only in case of 'mqsc' logs
267269
// Also if the logging source is from autocfgmqsc.LOG, then we have to construct the json string as per below logic
268270
if checkLogSourceForMirroring("mqsc") && canMQSCLogBeMirroredToConsole(msg) {
269271
logLevel := determineMQSCLogLevel(strings.TrimSpace(msg))
270-
fmt.Printf("{\"ibm_datetime\":\"%s\",\"type\":\"mqsc_log\",\"loglevel\":\"%s\",\"message\":\"%s\"}\n",
272+
mirrorLogWriter.Printf("{\"ibm_datetime\":\"%s\",\"type\":\"mqsc_log\",\"loglevel\":\"%s\",\"message\":\"%s\"}\n",
271273
getTimeStamp(), logLevel, strings.TrimSpace(msg))
272274
}
273275
}
274276
return true
275277
}, nil
276278
case "basic":
277-
log, err = logger.NewLogger(os.Stderr, d, false, name)
279+
log, err = logger.NewLogger(os.Stdout, d, false, name)
278280
if err != nil {
279281
return nil, err
280282
}
283+
mirrorLogWriter := syncwriter.For(os.Stdout)
281284
return func(msg string, isQMLog bool) bool {
282285
arrLoggingConsoleExcludeIds := strings.Split(strings.ToUpper(os.Getenv("MQ_LOGGING_CONSOLE_EXCLUDE_ID")), ",")
283286
if isExcludedMsgIdPresent(msg, arrLoggingConsoleExcludeIds) {
@@ -294,18 +297,18 @@ func configureLogger(name string) (mirrorFunc, error) {
294297
if err != nil {
295298
log.Printf("Failed to unmarshall JSON in log message - %v", err)
296299
} else {
297-
fmt.Print(formatBasic(obj))
300+
mirrorLogWriter.Print(formatBasic(obj))
298301
}
299302
} else {
300303
// The log being mirrored isn't JSON, so just print it. This can happen only in case of mqsc logs
301304
if checkLogSourceForMirroring("mqsc") && canMQSCLogBeMirroredToConsole(msg) {
302-
log.Printf("%s", strings.TrimSpace(msg))
305+
log.Print(strings.TrimSpace(msg))
303306
}
304307
}
305308
return true
306309
}, nil
307310
default:
308-
log, err = logger.NewLogger(os.Stdout, d, false, name)
311+
log, err = logger.NewLogger(os.Stderr, d, false, name)
309312
if err != nil {
310313
return nil, err
311314
}

internal/fips/fips_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ limitations under the License.
1818
package fips
1919

2020
import (
21-
"fmt"
2221
"os"
2322
"testing"
2423
)
@@ -35,7 +34,7 @@ func TestEnableFIPSAuto(t *testing.T) {
3534
func TestEnableFIPSTrue(t *testing.T) {
3635
// Test MQ_ENABLE_FIPS=true
3736
os.Setenv("MQ_ENABLE_FIPS", "true")
38-
fmt.Println(os.Getenv("MQ_ENABLE_FIPS"))
37+
t.Log(os.Getenv("MQ_ENABLE_FIPS"))
3938
ProcessFIPSType(nil)
4039
fipsType := IsFIPSEnabled()
4140
if !fipsType {

pkg/logger/logger.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ import (
2424
"os"
2525
"os/user"
2626
"strconv"
27-
"sync"
2827
"time"
28+
29+
"github.com/ibm-messaging/mq-container/pkg/syncwriter"
2930
)
3031

3132
// timestampFormat matches the format used by MQ messages (includes milliseconds)
@@ -36,8 +37,7 @@ const errorLevel string = "ERROR"
3637

3738
// A Logger is used to log messages to stdout
3839
type Logger struct {
39-
mutex sync.Mutex
40-
writer io.Writer
40+
writer *syncwriter.SyncWriter
4141
debug bool
4242
json bool
4343
processName string
@@ -62,8 +62,7 @@ func NewLogger(writer io.Writer, debug bool, json bool, serverName string) (*Log
6262
userName = user.Username
6363
}
6464
return &Logger{
65-
mutex: sync.Mutex{},
66-
writer: writer,
65+
writer: syncwriter.For(writer),
6766
debug: debug,
6867
json: json,
6968
processName: os.Args[0],
@@ -101,17 +100,14 @@ func (l *Logger) log(level string, msg string) {
101100
"type": "mq_containerlog",
102101
}
103102
s, err := l.format(entry)
104-
l.mutex.Lock()
105103
if err != nil {
106-
// TODO: Fix this
107-
fmt.Println(err)
104+
syncwriter.For(os.Stderr).Println(err)
108105
}
109106
if l.json {
110-
fmt.Fprintln(l.writer, s)
107+
l.writer.Println(s)
111108
} else {
112-
fmt.Fprint(l.writer, s)
109+
l.writer.Print(s)
113110
}
114-
l.mutex.Unlock()
115111
}
116112

117113
// Debug logs a line as debug

pkg/logger/logger_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func TestJSONLogger(t *testing.T) {
3232
s := "Hello world"
3333
l.Print(s)
3434
var e map[string]interface{}
35-
err = json.Unmarshal([]byte(buf.String()), &e)
35+
err = json.Unmarshal(buf.Bytes(), &e)
3636
if err != nil {
3737
t.Error(err)
3838
}

pkg/syncwriter/syncwriter.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package syncwriter
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"sync"
8+
)
9+
10+
var (
11+
syncWriters = map[io.Writer]*SyncWriter{}
12+
13+
loggerMutex = sync.Mutex{}
14+
sharedStdoutStderrLock = sync.Mutex{}
15+
)
16+
17+
type SyncWriter struct {
18+
writeLock *sync.Mutex
19+
output io.Writer
20+
}
21+
22+
func (s *SyncWriter) Write(p []byte) (n int, err error) {
23+
s.writeLock.Lock()
24+
defer s.writeLock.Unlock()
25+
26+
bytesWritten := 0
27+
for len(p) > 0 {
28+
n, err := s.output.Write(p)
29+
bytesWritten += n
30+
if err != nil {
31+
return bytesWritten, err
32+
}
33+
p = p[n:]
34+
}
35+
return bytesWritten, nil
36+
}
37+
38+
func (s *SyncWriter) Print(a ...any) { fmt.Fprint(s, a...) }
39+
func (s *SyncWriter) Println(a ...any) { fmt.Fprintln(s, a...) }
40+
func (s *SyncWriter) Printf(format string, a ...any) { fmt.Fprintf(s, format, a...) }
41+
42+
// For returns a SyncWriter for the given underlying writer.
43+
//
44+
// A separate SyncWriter will be created for each underlying writer but multiple calls supplying the same writer will return the same SyncWriter.
45+
//
46+
// Note: as a special case, stdout and stderr share a write lock to prevent race conditions where these streams are converged in container logs
47+
func For(w io.Writer) *SyncWriter {
48+
loggerMutex.Lock()
49+
defer loggerMutex.Unlock()
50+
51+
if writer, found := syncWriters[w]; found {
52+
return writer
53+
}
54+
var lock *sync.Mutex
55+
if w == os.Stdout || w == os.Stderr {
56+
lock = &sharedStdoutStderrLock
57+
} else {
58+
lock = &sync.Mutex{}
59+
}
60+
writer := &SyncWriter{
61+
output: w,
62+
writeLock: lock,
63+
}
64+
syncWriters[w] = writer
65+
return writer
66+
}

0 commit comments

Comments
 (0)