Skip to content

Commit ddca25d

Browse files
abhaygoudannavarurunc-bot[bot]
authored andcommitted
fix(metrics): fallback to mock writer on file open failure
When timestamps are enabled but the target file cannot be opened, NewZerologMetrics returned nil. This caused a nil pointer dereference on subsequent metrics.Capture() calls. This fix gracefully degrades to a no-op mockWriter and logs a warning. Adds a regression test to verify the fallback behavior. PR: #596 Fixes: #595 Signed-off-by: abhaygoudannavar <abhaysgoudnvr@gmail.com> Reviewed-by: Charalampos Mainas <cmainas@nubificus.co.uk> Approved-by: Charalampos Mainas <cmainas@nubificus.co.uk>
1 parent 06d845c commit ddca25d

3 files changed

Lines changed: 34 additions & 21 deletions

File tree

cmd/urunc/main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,10 @@ func main() {
140140
if err != nil {
141141
return nil, err
142142
}
143-
metrics = m.NewZerologMetrics(cfg.Timestamps.Enabled, cfg.Timestamps.Destination, "")
143+
metrics, err = m.NewZerologMetrics(cfg.Timestamps.Enabled, cfg.Timestamps.Destination, "")
144+
if err != nil {
145+
logrus.Warnf("Metrics will be disabled: %v", err)
146+
}
144147
return nil, nil
145148
},
146149
}

internal/metrics/metrics.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package metrics
1616

1717
import (
18+
"fmt"
1819
"os"
1920

2021
"github.com/rs/zerolog"
@@ -51,21 +52,23 @@ func (z *zerologMetrics) SetLoggerContainerID(containerID string) {
5152
z.containerID = containerID
5253
}
5354

54-
// NewZerologMetrics creates a Writer that logs timestamps for a single container
55-
func NewZerologMetrics(enabled bool, target string, containerID string) Writer {
55+
// NewZerologMetrics creates a Writer that logs timestamps for a single container.
56+
// On file open failure it returns a no-op mockWriter and an error, allowing the
57+
// caller to log or handle the error as appropriate.
58+
func NewZerologMetrics(enabled bool, target string, containerID string) (Writer, error) {
5659
if enabled {
5760
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixNano
5861
file, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
5962
if err != nil {
60-
return nil
63+
return &mockWriter{}, fmt.Errorf("failed to open metrics file %s: %w", target, err)
6164
}
6265
logger := zerolog.New(file).Level(zerolog.InfoLevel).With().Timestamp().Logger()
6366
return &zerologMetrics{
6467
logger: &logger,
6568
containerID: containerID,
66-
}
69+
}, nil
6770
}
68-
return &mockWriter{}
71+
return &mockWriter{}, nil
6972
}
7073

7174
type mockWriter struct{}

internal/metrics/metrics_test.go

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"testing"
2121

2222
"github.com/rs/zerolog"
23+
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
2325
)
2426

2527
func TestZerologMetricsMetadata(t *testing.T) {
@@ -35,20 +37,25 @@ func TestZerologMetricsMetadata(t *testing.T) {
3537

3638
line := buf.String()
3739
var m map[string]any
38-
if err := json.Unmarshal([]byte(line), &m); err != nil {
39-
t.Fatalf("failed to parse log: %v", err)
40-
}
40+
err := json.Unmarshal([]byte(line), &m)
41+
require.NoError(t, err, "failed to parse log output")
4142

42-
if got := m["containerID"]; got != "container00" {
43-
t.Errorf("containerID = %v, want container00", got)
44-
}
45-
if got := m["timestampID"]; got != "TS00" {
46-
t.Errorf("timestampID = %v, want TS00", got)
47-
}
48-
if got := m["timestampName"]; got != "CR.invoked" {
49-
t.Errorf("timestampName = %v, want CR.invoked", got)
50-
}
51-
if got := int(m["timestampOrder"].(float64)); got != 0 {
52-
t.Errorf("timestampOrder = %v, want 0", got)
53-
}
43+
assert.Equal(t, "container00", m["containerID"], "containerID mismatch")
44+
assert.Equal(t, "TS00", m["timestampID"], "timestampID mismatch")
45+
assert.Equal(t, "CR.invoked", m["timestampName"], "timestampName mismatch")
46+
assert.Equal(t, float64(0), m["timestampOrder"], "timestampOrder mismatch")
47+
}
48+
49+
func TestZerologMetricsInvalidFileDoesNotPanic(t *testing.T) {
50+
// Provide a path to a directory that definitely does not exist
51+
invalidPath := "/does/not/exist/timestamps.log"
52+
53+
// Create the metrics writer with timestamps enabled but an invalid path
54+
writer, err := NewZerologMetrics(true, invalidPath, "container-test")
55+
56+
require.Error(t, err, "expected an error for invalid file path")
57+
require.NotNil(t, writer, "expected non-nil writer (fallback to mockWriter), got nil")
58+
59+
_, isMock := writer.(*mockWriter)
60+
assert.True(t, isMock, "expected writer to be *mockWriter on file open failure")
5461
}

0 commit comments

Comments
 (0)