Skip to content

Commit 7a31fa4

Browse files
Andrew-Lees11GitHub Enterprise
authored andcommitted
Prevent races when writing to stdout/stderr (#1031)
1 parent 3bbed2c commit 7a31fa4

File tree

9 files changed

+113
-35
lines changed

9 files changed

+113
-35
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Change log
22

3+
## 9.4.3.1-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+
7+
## 9.4.3.1-r1 (2025-08)
8+
9+
* Updated to MQ version 9.4.3.1
10+
311
## 9.4.3.0 (2025-06)
412

513
* Updated to MQ version 9.4.3.0

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
@@ -34,6 +34,7 @@ import (
3434
"github.com/ibm-messaging/mq-container/internal/pathutils"
3535
"github.com/ibm-messaging/mq-container/pkg/logger"
3636
"github.com/ibm-messaging/mq-container/pkg/mqini"
37+
"github.com/ibm-messaging/mq-container/pkg/syncwriter"
3738
)
3839

3940
// var debug = false
@@ -288,10 +289,11 @@ func configureLogger(name string) (mirrorFunc, error) {
288289
d := getDebug()
289290
switch f {
290291
case "json":
291-
log, err = logger.NewLogger(os.Stderr, d, true, name)
292+
log, err = logger.NewLogger(os.Stdout, d, true, name)
292293
if err != nil {
293294
return nil, err
294295
}
296+
mirrorLogWriter := syncwriter.For(os.Stdout)
295297
return func(msg string, isQMLog bool) bool {
296298
arrLoggingConsoleExcludeIds := strings.Split(strings.ToUpper(os.Getenv("MQ_LOGGING_CONSOLE_EXCLUDE_ID")), ",")
297299
if isExcludedMsgIdPresent(msg, arrLoggingConsoleExcludeIds) {
@@ -307,24 +309,25 @@ func configureLogger(name string) (mirrorFunc, error) {
307309
if err != nil {
308310
log.Printf("Failed to unmarshall JSON in log message - %v", msg)
309311
} else {
310-
fmt.Println(msg)
312+
mirrorLogWriter.Println(msg)
311313
}
312314
} else {
313315
// The log being mirrored isn't JSON. This can happen only in case of 'mqsc' logs
314316
// Also if the logging source is from autocfgmqsc.LOG, then we have to construct the json string as per below logic
315317
if checkLogSourceForMirroring("mqsc") && canMQSCLogBeMirroredToConsole(msg) {
316318
logLevel := determineMQSCLogLevel(strings.TrimSpace(msg))
317-
fmt.Printf("{\"ibm_datetime\":\"%s\",\"type\":\"mqsc_log\",\"loglevel\":\"%s\",\"message\":\"%s\"}\n",
319+
mirrorLogWriter.Printf("{\"ibm_datetime\":\"%s\",\"type\":\"mqsc_log\",\"loglevel\":\"%s\",\"message\":\"%s\"}\n",
318320
getTimeStamp(), logLevel, strings.TrimSpace(msg))
319321
}
320322
}
321323
return true
322324
}, nil
323325
case "basic":
324-
log, err = logger.NewLogger(os.Stderr, d, false, name)
326+
log, err = logger.NewLogger(os.Stdout, d, false, name)
325327
if err != nil {
326328
return nil, err
327329
}
330+
mirrorLogWriter := syncwriter.For(os.Stdout)
328331
return func(msg string, isQMLog bool) bool {
329332
arrLoggingConsoleExcludeIds := strings.Split(strings.ToUpper(os.Getenv("MQ_LOGGING_CONSOLE_EXCLUDE_ID")), ",")
330333
if isExcludedMsgIdPresent(msg, arrLoggingConsoleExcludeIds) {
@@ -341,18 +344,18 @@ func configureLogger(name string) (mirrorFunc, error) {
341344
if err != nil {
342345
log.Printf("Failed to unmarshall JSON in log message - %v", err)
343346
} else {
344-
fmt.Print(formatBasic(obj))
347+
mirrorLogWriter.Print(formatBasic(obj))
345348
}
346349
} else {
347350
// The log being mirrored isn't JSON, so just print it. This can happen only in case of mqsc logs
348351
if checkLogSourceForMirroring("mqsc") && canMQSCLogBeMirroredToConsole(msg) {
349-
log.Printf("%s", strings.TrimSpace(msg))
352+
log.Print(strings.TrimSpace(msg))
350353
}
351354
}
352355
return true
353356
}, nil
354357
default:
355-
log, err = logger.NewLogger(os.Stdout, d, false, name)
358+
log, err = logger.NewLogger(os.Stderr, d, false, name)
356359
if err != nil {
357360
return nil, err
358361
}

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 {

internal/metrics/audit.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
"net/http"
2323
"os"
2424
"time"
25+
26+
"github.com/ibm-messaging/mq-container/pkg/syncwriter"
2527
)
2628

2729
type auditEvent struct {
@@ -63,7 +65,7 @@ func newAuditingHandlerFuncWrapper(qmName string, logger logHandler) handlerFunc
6365
eventBytes, err := json.Marshal(event)
6466
if err != nil {
6567
logger.Append(fmt.Sprintf("Error writing audit log; next event may contain incomplete data: %s", err.Error()), false)
66-
fmt.Printf("Error constructing audit log event: %s\n", err.Error())
68+
syncwriter.For(os.Stderr).Printf("Error constructing audit log event: %s\n", err.Error())
6769
}
6870
logger.Append(string(eventBytes), false)
6971
}

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/logrotation/logrotation.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import (
2525
"slices"
2626
"strings"
2727
"sync"
28+
29+
"github.com/ibm-messaging/mq-container/pkg/syncwriter"
2830
)
2931

3032
type RotatingLogger struct {
@@ -86,24 +88,26 @@ func (r *RotatingLogger) Append(messageLine string, deduplicateLine bool) {
8688
// we will always log in the first instance of the log files
8789
logFilePath := r.instanceFileName(1)
8890

91+
errOutput := syncwriter.For(os.Stderr)
92+
8993
// open the log file in append mode
9094
// for the gosec rule Id: G302 - Expect file permissions to be 0600 or less
9195
logFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
9296
if err != nil {
93-
fmt.Printf("Failed to open log file: %v\n", err)
97+
errOutput.Printf("Failed to open log file: %v\n", err)
9498
return
9599
}
96100

97101
defer func(f *os.File) {
98102
if err := logFile.Close(); err != nil {
99-
fmt.Printf("Error: %v, Failed to close log file: %s\n", err, logFilePath)
103+
errOutput.Printf("Error: %v, Failed to close log file: %s\n", err, logFilePath)
100104
}
101105
}(logFile)
102106

103107
// check if the message should be appended to the file
104108
shouldBeAppended, err := r.checkIfMessageShouldBeAppended(logFilePath, messageLine, deduplicateLine)
105109
if err != nil {
106-
fmt.Printf("Failed to validate the currentLog and the lastLog line %v\n", err)
110+
errOutput.Printf("Failed to validate the currentLog and the lastLog line %v\n", err)
107111
}
108112

109113
if !shouldBeAppended {
@@ -113,7 +117,7 @@ func (r *RotatingLogger) Append(messageLine string, deduplicateLine bool) {
113117
// check if the logFileSize has exceeded the maxFileSize then perform the logrotation
114118
logFileSizeExceeded, err := r.checkIfLogFileSizeExceeded(len(messageLine), logFile)
115119
if err != nil {
116-
fmt.Printf("Failed to validate log file size: %v\n", err)
120+
errOutput.Printf("Failed to validate log file size: %v\n", err)
117121
return
118122
}
119123

@@ -122,33 +126,33 @@ func (r *RotatingLogger) Append(messageLine string, deduplicateLine bool) {
122126
// close the current log file
123127
err = logFile.Close()
124128
if err != nil {
125-
fmt.Printf("Error: %v, Failed to close log file: %v\n", err, logFile.Name())
129+
errOutput.Printf("Error: %v, Failed to close log file: %v\n", err, logFile.Name())
126130
}
127131

128132
// perform log rotation
129133
err = r.performLogRotation()
130134
if err != nil {
131-
fmt.Printf("Failed to perform log-rotation: %v\n", err)
135+
errOutput.Printf("Failed to perform log-rotation: %v\n", err)
132136
}
133137

134138
// open the newly created logFile
135139
logFile, err = os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
136140
if err != nil {
137-
fmt.Printf("Failed to open log file: %v\n", err)
141+
errOutput.Printf("Failed to open log file: %v\n", err)
138142
return
139143
}
140144

141145
defer func(f *os.File) {
142146
if err := logFile.Close(); err != nil {
143-
fmt.Printf("Error: %v, Failed to close log file: %s\n", err, logFilePath)
147+
errOutput.Printf("Error: %v, Failed to close log file: %s\n", err, logFilePath)
144148
}
145149
}(logFile)
146150
}
147151

148152
// append the message to the file
149153
_, err = logFile.WriteString(messageLine)
150154
if err != nil {
151-
fmt.Printf("Failed to write to log file: %v\n", err)
155+
errOutput.Printf("Failed to write to log file: %v\n", err)
152156
}
153157

154158
}
@@ -225,7 +229,7 @@ func (r *RotatingLogger) getLogLastLine(logFilePath string) (string, error) {
225229

226230
defer func() {
227231
if err := logFile.Close(); err != nil {
228-
fmt.Printf("error closing logfile: %s", logFilePath)
232+
syncwriter.For(os.Stderr).Printf("error closing logfile: %s", logFilePath)
229233
}
230234
}()
231235

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)