zapjournald/zapjournald.go
Evgenii Stratonikov d6ea4d0bbf [#12] Use buffer pool for message encoding
```
goos: linux
goarch: amd64
pkg: git.frostfs.info/TrueCloudLab/zapjournald
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
                                     │      1       │                  2                  │
                                     │    sec/op    │   sec/op     vs base                │
Logger/standard/no_fields-8             450.3n ± 2%   452.9n ± 3%        ~ (p=0.138 n=10)
Logger/standard/application_fields-8    610.4n ± 1%   608.4n ± 1%        ~ (p=0.325 n=10)
Logger/standard/journald_fields-8       595.8n ± 5%   604.5n ± 1%        ~ (p=0.075 n=10)
Logger/journald/no_fields-8            1809.0n ± 4%   900.4n ± 1%  -50.23% (p=0.000 n=10)
Logger/journald/application_fields-8    2.037µ ± 5%   1.087µ ± 1%  -46.62% (p=0.000 n=10)
Logger/journald/journald_fields-8       2.054µ ± 4%   1.119µ ± 1%  -45.52% (p=0.000 n=10)
geomean                                 1.036µ        753.1n       -27.33%

                                     │        1        │                  2                   │
                                     │      B/op       │    B/op     vs base                  │
Logger/standard/no_fields-8               0.000 ± 0%     0.000 ± 0%        ~ (p=1.000 n=10) ¹
Logger/standard/application_fields-8      128.0 ± 0%     128.0 ± 0%        ~ (p=1.000 n=10) ¹
Logger/standard/journald_fields-8         64.00 ± 0%     64.00 ± 0%        ~ (p=1.000 n=10) ¹
Logger/journald/no_fields-8            1206.000 ± 0%     5.000 ± 0%  -99.59% (p=0.000 n=10)
Logger/journald/application_fields-8     1494.0 ± 0%     133.0 ± 0%  -91.10% (p=0.000 n=10)
Logger/journald/journald_fields-8       1478.00 ± 0%     85.00 ± 0%  -94.25% (p=0.000 n=10)
geomean                                              ²               -83.36%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                     │       1       │                  2                   │
                                     │   allocs/op   │ allocs/op   vs base                  │
Logger/standard/no_fields-8             0.000 ± 0%     0.000 ± 0%        ~ (p=1.000 n=10) ¹
Logger/standard/application_fields-8    1.000 ± 0%     1.000 ± 0%        ~ (p=1.000 n=10) ¹
Logger/standard/journald_fields-8       1.000 ± 0%     1.000 ± 0%        ~ (p=1.000 n=10) ¹
Logger/journald/no_fields-8            29.000 ± 0%     1.000 ± 0%  -96.55% (p=0.000 n=10)
Logger/journald/application_fields-8   30.000 ± 0%     2.000 ± 0%  -93.33% (p=0.000 n=10)
Logger/journald/journald_fields-8      31.000 ± 0%     3.000 ± 0%  -90.32% (p=0.000 n=10)
geomean                                            ²               -75.38%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean
```

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-10-18 11:30:00 +03:00

188 lines
5 KiB
Go

package zapjournald
import (
"fmt"
"strconv"
"github.com/ssgreg/journald"
"go.uber.org/zap/zapcore"
"golang.org/x/exp/maps"
)
// Core for zapjournald.
//
// Implements zapcore.LevelEnabler and zapcore.Core interfaces.
type Core struct {
zapcore.LevelEnabler
encoder zapcore.Encoder
j *journald.Journal
// field names, which will be stored in journald structure
storedFieldNames map[string]struct{}
// journald fields, which are always present in current core context
contextStructuredFields map[string]interface{}
}
func NewCore(enab zapcore.LevelEnabler, encoder zapcore.Encoder, journal *journald.Journal, journalFields []string) *Core {
journalFieldsMap := make(map[string]struct{})
for _, field := range journalFields {
journalFieldsMap[field] = struct{}{}
}
return &Core{
LevelEnabler: enab,
encoder: encoder,
j: journal,
storedFieldNames: journalFieldsMap,
contextStructuredFields: make(map[string]interface{}),
}
}
// With adds structured context to the Core.
func (core *Core) With(fields []zapcore.Field) zapcore.Core {
clone := core.clone()
for _, field := range fields {
field.AddTo(clone.encoder)
clone.contextStructuredFields[field.Key] = getFieldValue(field)
}
return clone
}
// Check determines whether the supplied Entry should be logged (using the
// embedded LevelEnabler and possibly some extra logic). If the entry
// should be logged, the Core adds itself to the CheckedEntry and returns
// the result.
//
// Callers must use Check before calling Write.
func (core *Core) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if core.Enabled(entry.Level) {
return checked.AddCore(entry, core)
}
return checked
}
// Write serializes the Entry and any Fields supplied at the log site and
// writes them to their destination.
//
// If called, Write should always log the Entry and Fields; it should not
// replicate the logic of Check.
func (core *Core) Write(entry zapcore.Entry, fields []zapcore.Field) error {
prio, err := zapLevelToJournald(entry.Level)
if err != nil {
return err
}
b := pool.Get()
defer b.Free()
writeField(b, "PRIORITY", strconv.Itoa(int(prio)))
structuredFields := maps.Clone(core.contextStructuredFields)
for _, field := range fields {
if _, isJournalField := core.storedFieldNames[field.Key]; isJournalField {
structuredFields[field.Key] = getFieldValue(field)
}
}
for k, v := range structuredFields {
switch v := v.(type) {
case []byte:
writeFieldBytes(b, k, v)
case string:
writeField(b, k, v)
default:
writeField(b, k, fmt.Sprint(v))
}
}
// Generate the message.
buffer, err := core.encoder.EncodeEntry(entry, fields)
if err != nil {
return fmt.Errorf("failed to encode log entry: %w", err)
}
defer buffer.Free()
writeFieldBytes(b, "MESSAGE", buffer.Bytes())
// Write the message.
return core.j.WriteMsg(b.Bytes())
}
// Sync flushes buffered logs (not used).
func (core *Core) Sync() error {
return nil
}
// clone returns clone of core.
func (core *Core) clone() *Core {
return &Core{
LevelEnabler: core.LevelEnabler,
encoder: core.encoder.Clone(),
j: core.j,
storedFieldNames: maps.Clone(core.storedFieldNames),
contextStructuredFields: maps.Clone(core.contextStructuredFields),
}
}
// getFieldValue returns underlying value stored in zapcore.Field.
func getFieldValue(f zapcore.Field) interface{} {
switch f.Type {
case zapcore.ArrayMarshalerType,
zapcore.ObjectMarshalerType,
zapcore.InlineMarshalerType,
zapcore.BinaryType,
zapcore.ByteStringType,
zapcore.Complex128Type,
zapcore.Complex64Type,
zapcore.TimeFullType,
zapcore.ReflectType,
zapcore.NamespaceType,
zapcore.StringerType,
zapcore.ErrorType,
zapcore.SkipType:
return f.Interface
case zapcore.DurationType,
zapcore.Float64Type,
zapcore.Float32Type,
zapcore.Int64Type,
zapcore.Int32Type,
zapcore.Int16Type,
zapcore.Int8Type,
zapcore.Uint64Type,
zapcore.Uint32Type,
zapcore.Uint16Type,
zapcore.Uint8Type,
zapcore.UintptrType,
zapcore.BoolType:
return f.Integer
case zapcore.StringType:
return f.String
case zapcore.TimeType:
if f.Interface != nil {
// for example: zap.Time("k", time.Unix(100900, 0).In(time.UTC)) - will produce: "100900000000000 UTC" (result in nanoseconds)
return fmt.Sprintf("%d %v", f.Integer, f.Interface)
}
return f.Integer
default:
panic(fmt.Sprintf("unknown field type: %v", f))
}
}
func zapLevelToJournald(l zapcore.Level) (journald.Priority, error) {
switch l {
case zapcore.DebugLevel:
return journald.PriorityDebug, nil
case zapcore.InfoLevel:
return journald.PriorityInfo, nil
case zapcore.WarnLevel:
return journald.PriorityWarning, nil
case zapcore.ErrorLevel:
return journald.PriorityErr, nil
case zapcore.DPanicLevel:
return journald.PriorityCrit, nil
case zapcore.PanicLevel:
return journald.PriorityCrit, nil
case zapcore.FatalLevel:
return journald.PriorityCrit, nil
default:
return 0, fmt.Errorf("unknown log level: %v", l)
}
}