Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Container for building Go binary.
FROM golang:1.26.5-trixie AS builder
FROM golang:1.26.6-trixie AS builder
# Install dependencies
RUN apt-get update && apt-get install -y --no-install-recommends build-essential git

Expand Down
5 changes: 3 additions & 2 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ func wireCoreWorkflow(ctx context.Context, life *lifecycle.Manager, conf Config,
// Priority protocol always uses QBFTv2.
isync, err := wirePrioritise(ctx, conf, life, p2pNode, peerIDs, lock.Threshold,
sender.SendReceive, defaultConsensus, sched, p2pKey, deadlineFunc,
consensusController, lock.ConsensusProtocol)
consensusController, lock.ConsensusProtocol, gaterFunc)
if err != nil {
return err
}
Expand Down Expand Up @@ -773,6 +773,7 @@ func wirePrioritise(ctx context.Context, conf Config, life *lifecycle.Manager, p
peers []peer.ID, threshold int, sendFunc p2p.SendReceiveFunc, coreCons core.Consensus,
sched core.Scheduler, p2pKey *k1.PrivateKey, deadlineFunc func(duty core.Duty) (time.Time, bool),
consensusController core.ConsensusController, clusterPreferredProtocol string,
gaterFunc core.DutyGaterFunc,
) (*infosync.Component, error) {
cons, ok := coreCons.(*qbft.Consensus)
if !ok {
Expand All @@ -785,7 +786,7 @@ func wirePrioritise(ctx context.Context, conf Config, life *lifecycle.Manager, p
const exchangeTimeout = time.Second * 6

prio, err := priority.NewComponent(ctx, p2pNode, peers, threshold,
sendFunc, p2p.RegisterHandler, cons, exchangeTimeout, p2pKey, deadlineFunc)
sendFunc, p2p.RegisterHandler, cons, exchangeTimeout, p2pKey, deadlineFunc, gaterFunc)
if err != nil {
return nil, err
}
Expand Down
40 changes: 26 additions & 14 deletions app/log/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,39 +264,51 @@ func NewConsoleForT(_ *testing.T, ws zapcore.WriteSyncer, opts ...func(*zapcore.
}

// InitConsoleForT initialises a global console logger for testing purposes.
// The previous global logger is restored on test cleanup.
func InitConsoleForT(t *testing.T, ws zapcore.WriteSyncer, opts ...func(*zapcore.EncoderConfig)) {
t.Helper()

initMu.Lock()
defer initMu.Unlock()

logger = NewConsoleForT(t, ws, opts...)
setLoggerForT(t, NewConsoleForT(t, zapcore.Lock(ws), opts...))
}

// InitJSONForT initialises a json logger for testing purposes.
// The previous global logger is restored on test cleanup.
func InitJSONForT(t *testing.T, ws zapcore.WriteSyncer, opts ...func(*zapcore.EncoderConfig)) {
t.Helper()

initMu.Lock()
defer initMu.Unlock()

var err error

logger, err = newStructuredLogger("json", zapcore.DebugLevel, true, ws, defaultCallerSkip, opts...)
l, err := newStructuredLogger("json", zapcore.DebugLevel, true, zapcore.Lock(ws), defaultCallerSkip, opts...)
require.NoError(t, err)

setLoggerForT(t, l)
}

// InitLogfmtForT initialises a logfmt logger for testing purposes.
// The previous global logger is restored on test cleanup.
func InitLogfmtForT(t *testing.T, ws zapcore.WriteSyncer, opts ...func(*zapcore.EncoderConfig)) {
t.Helper()

l, err := newStructuredLogger("logfmt", zapcore.DebugLevel, false, zapcore.Lock(ws), defaultCallerSkip, opts...)
require.NoError(t, err)

setLoggerForT(t, l)
}

// setLoggerForT sets the global logger and restores the previous logger on test cleanup.
// Note the write syncer must be safe for concurrent use since logging may happen from multiple goroutines.
func setLoggerForT(t *testing.T, l zapLogger) {
t.Helper()

initMu.Lock()
defer initMu.Unlock()

var err error
prev := logger
logger = l

logger, err = newStructuredLogger("logfmt", zapcore.DebugLevel, false, ws, defaultCallerSkip, opts...)
require.NoError(t, err)
t.Cleanup(func() {
initMu.Lock()
defer initMu.Unlock()

logger = prev
})
}

// Stop stops all log processors.
Expand Down
53 changes: 53 additions & 0 deletions app/log/config_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import (
"net/http"
"net/http/httptest"
"strconv"
"sync"
"testing"

"github.com/golang/snappy"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest"
"google.golang.org/protobuf/proto"

pbv1 "github.com/obolnetwork/charon/app/log/loki/lokipb/v1"
Expand Down Expand Up @@ -68,6 +71,56 @@ func TestLokiCaller(t *testing.T) {
<-done
}

var initForTFuncs = map[string]func(*testing.T, zapcore.WriteSyncer, ...func(*zapcore.EncoderConfig)){
"console": InitConsoleForT,
"logfmt": InitLogfmtForT,
"json": InitJSONForT,
}

func TestInitForTRestoresLogger(t *testing.T) {
for name, initFunc := range initForTFuncs {
t.Run(name, func(t *testing.T) {
var buf zaptest.Buffer

t.Run("install", func(t *testing.T) {
initFunc(t, &buf)
Debug(context.Background(), "inside test")
require.Contains(t, buf.String(), "inside test")
})

// The previous logger must be restored on test cleanup,
// so this log must not be written to the buffer.
lenBefore := buf.Len()

Debug(context.Background(), "after test")
require.Equal(t, lenBefore, buf.Len())
})
}
}

func TestInitForTConcurrentLogging(t *testing.T) {
for name, initFunc := range initForTFuncs {
t.Run(name, func(t *testing.T) {
// zaptest.Buffer is not safe for concurrent use, the
// initialisers must synchronise writes to it.
var buf zaptest.Buffer

initFunc(t, &buf)

var wg sync.WaitGroup
for range 8 {
wg.Go(func() {
for range 100 {
Debug(context.Background(), "concurrent log")
}
})
}

wg.Wait()
})
}
}

func decode(t *testing.T, b []byte) *pbv1.PushRequest {
t.Helper()

Expand Down
2 changes: 1 addition & 1 deletion app/log/loki/lokipb/v1/loki.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 22 additions & 1 deletion app/log/slog.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
// Copyright © 2022-2026 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1

//nolint:revive,nolintlint // somehow the nolintlint linter catches revive as unnecessary, while it is
package log

import (
"context"
"fmt"
"log/slog"
"os"
"runtime"
"strings"
"sync"

"go.uber.org/zap/zapcore"

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/z"
)

// SlogHandler returns a slog.Handler that writes records to the global charon logger.
Expand Down Expand Up @@ -43,6 +48,20 @@ func (h *slogHandler) Enabled(_ context.Context, level slog.Level) bool {
}

func (h *slogHandler) Handle(_ context.Context, rec slog.Record) error {
// Never let a logging panic crash the process.
defer func() {
if r := recover(); r != nil {
defer func() {
if r2 := recover(); r2 != nil {
fmt.Fprintf(os.Stderr, "slog handler panic (logging also failed): %v\n", r)
}
}()

Error(context.Background(), "Libp2p slog handler panic, log line dropped",
errors.New("slog handler panic", z.Str("panic", fmt.Sprint(r))))
}
}()

entry := zapcore.Entry{
Level: toZapLevel(rec.Level),
Time: rec.Time,
Expand Down Expand Up @@ -105,13 +124,15 @@ func (h *slogHandler) clone() *slogHandler {
}

// toZapField converts a slog attribute to a zap field, prefixing open group names.
// All values are stringified to avoid zapcore.ReflectType, which panics in the
// logfmt encoder on named types (e.g. protocol.ID).
func (h *slogHandler) toZapField(a slog.Attr) zapcore.Field {
key := a.Key
if len(h.groups) > 0 {
key = strings.Join(h.groups, ".") + "." + key
}

return zapcore.Field{Key: key, Type: zapcore.ReflectType, Interface: a.Value.Resolve().Any()}
return zapcore.Field{Key: key, Type: zapcore.StringType, String: fmt.Sprint(a.Value.Resolve().Any())}
}

// toZapLevel maps a slog level to the closest zap level.
Expand Down
38 changes: 38 additions & 0 deletions app/log/slog_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,41 @@ func TestSlogHandler(t *testing.T) {
other.Error("failed to listen", "err", "address in use")
require.Contains(t, buf.String(), "failed to listen")
}

// namedString is a named string type like protocol.ID that is not plain string.
type namedString string

func TestSlogHandlerNamedTypes(t *testing.T) {
var buf bytes.Buffer

InitLogfmtForT(t, zapcore.AddSync(&buf))

levels := parseSlogLevels("identify=debug")
h := slog.Handler(&slogHandler{levels: levels, level: levels.fallback})
identify := slog.New(h.WithAttrs([]slog.Attr{slog.String("logger", "identify")}))

// Logging a slice of named-string types (like []protocol.ID) previously
// panicked because logfmt's AppendReflected asserts interface{} to string.
protocols := []namedString{"/proto/1.0", "/proto/2.0"}

require.NotPanics(t, func() {
identify.Debug("sending identify", "protocols", protocols)
})

require.Contains(t, buf.String(), "sending identify")
require.Contains(t, buf.String(), "/proto/1.0")

// Float, int, and bool values must also survive logfmt encoding.
buf.Reset()
require.NotPanics(t, func() {
identify.Debug("peer stats",
"score", 3.14,
"conns", 42,
"relay", true,
)
})

require.Contains(t, buf.String(), "3.14")
require.Contains(t, buf.String(), "42")
require.Contains(t, buf.String(), "true")
}
2 changes: 1 addition & 1 deletion app/peerinfo/peerinfopb/v1/peerinfo.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/protonil/testdata/v1/test.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading