From 4bb73514e988c776a977731370528dd53757a355 Mon Sep 17 00:00:00 2001 From: Vasiliy Tolstov Date: Fri, 24 Oct 2025 12:05:04 +0300 Subject: [PATCH] merge v4 changes Signed-off-by: Vasiliy Tolstov --- logger/level.go | 18 ++++++++---- logger/options.go | 14 +++++++++ logger/slog/slog.go | 14 ++++++--- logger/slog/slog_test.go | 57 +++++++++++++++++++++++++++++++----- logger/unwrap/unwrap.go | 2 +- logger/unwrap/unwrap_test.go | 2 +- meter/meter.go | 56 ++++++++++++++++++++++++++++++----- meter/meter_test.go | 5 ++-- meter/noop.go | 21 +++++++++++++ meter/options.go | 12 ++++---- 10 files changed, 167 insertions(+), 34 deletions(-) diff --git a/logger/level.go b/logger/level.go index c51f4173..92832f29 100644 --- a/logger/level.go +++ b/logger/level.go @@ -4,18 +4,20 @@ package logger type Level int8 const ( - // TraceLevel level usually used to find bugs, very verbose + // TraceLevel usually used to find bugs, very verbose TraceLevel Level = iota - 2 - // DebugLevel level used only when enabled debugging + // DebugLevel used only when enabled debugging DebugLevel - // InfoLevel level used for general info about what's going on inside the application + // InfoLevel used for general info about what's going on inside the application InfoLevel - // WarnLevel level used for non-critical entries + // WarnLevel used for non-critical entries WarnLevel - // ErrorLevel level used for errors that should definitely be noted + // ErrorLevel used for errors that should definitely be noted ErrorLevel - // FatalLevel level used for critical errors and then calls `os.Exit(1)` + // FatalLevel used for critical errors and then calls `os.Exit(1)` FatalLevel + // NoneLevel used to disable logging + NoneLevel ) // String returns logger level string representation @@ -33,6 +35,8 @@ func (l Level) String() string { return "error" case FatalLevel: return "fatal" + case NoneLevel: + return "none" } return "info" } @@ -58,6 +62,8 @@ func ParseLevel(lvl string) Level { return ErrorLevel case FatalLevel.String(): return FatalLevel + case NoneLevel.String(): + return NoneLevel } return InfoLevel } diff --git a/logger/options.go b/logger/options.go index c96612a7..57927ab4 100644 --- a/logger/options.go +++ b/logger/options.go @@ -52,6 +52,12 @@ type Options struct { AddStacktrace bool // DedupKeys deduplicate keys in log output DedupKeys bool + // FatalFinalizers runs in order in [logger.Fatal] method + FatalFinalizers []func(context.Context) +} + +var DefaultFatalFinalizer = func(ctx context.Context) { + os.Exit(1) } // NewOptions creates new options struct @@ -65,6 +71,7 @@ func NewOptions(opts ...Option) Options { AddSource: true, TimeFunc: time.Now, Meter: meter.DefaultMeter, + FatalFinalizers: []func(context.Context){DefaultFatalFinalizer}, } WithMicroKeys()(&options) @@ -76,6 +83,13 @@ func NewOptions(opts ...Option) Options { return options } +// WithFatalFinalizers set logger.Fatal finalizers +func WithFatalFinalizers(fncs ...func(context.Context)) Option { + return func(o *Options) { + o.FatalFinalizers = fncs + } +} + // WithContextAttrFuncs appends default funcs for the context attrs filler func WithContextAttrFuncs(fncs ...ContextAttrFunc) Option { return func(o *Options) { diff --git a/logger/slog/slog.go b/logger/slog/slog.go index de9e1df2..6e8284e4 100644 --- a/logger/slog/slog.go +++ b/logger/slog/slog.go @@ -4,14 +4,12 @@ import ( "context" "io" "log/slog" - "os" "reflect" "regexp" "runtime" "strconv" "sync" "sync/atomic" - "time" "go.unistack.org/micro/v3/logger" "go.unistack.org/micro/v3/semconv" @@ -34,6 +32,7 @@ var ( warnValue = slog.StringValue("warn") errorValue = slog.StringValue("error") fatalValue = slog.StringValue("fatal") + noneValue = slog.StringValue("none") ) type wrapper struct { @@ -85,6 +84,8 @@ func (s *slogLogger) renameAttr(_ []string, a slog.Attr) slog.Attr { a.Value = errorValue case lvl >= logger.FatalLevel: a.Value = fatalValue + case lvl >= logger.NoneLevel: + a.Value = noneValue default: a.Value = infoValue } @@ -228,11 +229,12 @@ func (s *slogLogger) Error(ctx context.Context, msg string, attrs ...interface{} func (s *slogLogger) Fatal(ctx context.Context, msg string, attrs ...interface{}) { s.printLog(ctx, logger.FatalLevel, msg, attrs...) + for _, fn := range s.opts.FatalFinalizers { + fn(ctx) + } if closer, ok := s.opts.Out.(io.Closer); ok { closer.Close() } - time.Sleep(1 * time.Second) - os.Exit(1) } func (s *slogLogger) Warn(ctx context.Context, msg string, attrs ...interface{}) { @@ -316,6 +318,8 @@ func loggerToSlogLevel(level logger.Level) slog.Level { return slog.LevelDebug - 1 case logger.FatalLevel: return slog.LevelError + 1 + case logger.NoneLevel: + return slog.LevelError + 2 default: return slog.LevelInfo } @@ -333,6 +337,8 @@ func slogToLoggerLevel(level slog.Level) logger.Level { return logger.TraceLevel case slog.LevelError + 1: return logger.FatalLevel + case slog.LevelError + 2: + return logger.NoneLevel default: return logger.InfoLevel } diff --git a/logger/slog/slog_test.go b/logger/slog/slog_test.go index 2d605058..6f09ef5b 100644 --- a/logger/slog/slog_test.go +++ b/logger/slog/slog_test.go @@ -36,6 +36,24 @@ func TestStacktrace(t *testing.T) { } } +func TestNoneLevel(t *testing.T) { + ctx := context.TODO() + buf := bytes.NewBuffer(nil) + l := NewLogger(logger.WithLevel(logger.NoneLevel), logger.WithOutput(buf), + WithHandlerFunc(slog.NewTextHandler), + logger.WithAddStacktrace(true), + ) + if err := l.Init(logger.WithFields("key1", "val1")); err != nil { + t.Fatal(err) + } + + l.Error(ctx, "msg1", errors.New("err")) + + if buf.Len() != 0 { + t.Fatalf("logger none level not works, buf contains: %s", buf.Bytes()) + } +} + func TestDelayedBuffer(t *testing.T) { ctx := context.TODO() buf := bytes.NewBuffer(nil) @@ -405,15 +423,16 @@ func TestLogger(t *testing.T) { func Test_WithContextAttrFunc(t *testing.T) { loggerContextAttrFuncs := []logger.ContextAttrFunc{ func(ctx context.Context) []interface{} { - md, ok := metadata.FromIncomingContext(ctx) + md, ok := metadata.FromOutgoingContext(ctx) if !ok { return nil } attrs := make([]interface{}, 0, 10) for k, v := range md { - switch k { - case "X-Request-Id", "Phone", "External-Id", "Source-Service", "X-App-Install-Id", "Client-Id", "Client-Ip": - attrs = append(attrs, strings.ToLower(k), v) + key := strings.ToLower(k) + switch key { + case "x-request-id", "phone", "external-Id", "source-service", "x-app-install-id", "client-id", "client-ip": + attrs = append(attrs, key, v) } } return attrs @@ -423,7 +442,7 @@ func Test_WithContextAttrFunc(t *testing.T) { logger.DefaultContextAttrFuncs = append(logger.DefaultContextAttrFuncs, loggerContextAttrFuncs...) ctx := context.TODO() - ctx = metadata.AppendIncomingContext(ctx, "X-Request-Id", uuid.New().String(), + ctx = metadata.AppendOutgoingContext(ctx, "X-Request-Id", uuid.New().String(), "Source-Service", "Test-System") buf := bytes.NewBuffer(nil) @@ -436,17 +455,39 @@ func Test_WithContextAttrFunc(t *testing.T) { if !(bytes.Contains(buf.Bytes(), []byte(`"level":"info"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"test message"`))) { t.Fatalf("logger info, buf %s", buf.Bytes()) } - if !(bytes.Contains(buf.Bytes(), []byte(`"x-request-id":"`))) { + if !(bytes.Contains(buf.Bytes(), []byte(`"x-request-id":`))) { t.Fatalf("logger info, buf %s", buf.Bytes()) } if !(bytes.Contains(buf.Bytes(), []byte(`"source-service":"Test-System"`))) { t.Fatalf("logger info, buf %s", buf.Bytes()) } buf.Reset() - imd, _ := metadata.FromIncomingContext(ctx) + omd, _ := metadata.FromOutgoingContext(ctx) l.Info(ctx, "test message1") - imd.Set("Source-Service", "Test-System2") + omd.Set("Source-Service", "Test-System2") l.Info(ctx, "test message2") // t.Logf("xxx %s", buf.Bytes()) } + +func TestFatalFinalizers(t *testing.T) { + ctx := context.TODO() + buf := bytes.NewBuffer(nil) + l := NewLogger( + logger.WithLevel(logger.TraceLevel), + logger.WithOutput(buf), + ) + if err := l.Init( + logger.WithFatalFinalizers(func(ctx context.Context) { + l.Info(ctx, "fatal finalizer") + })); err != nil { + t.Fatal(err) + } + l.Fatal(ctx, "info_msg1") + if !bytes.Contains(buf.Bytes(), []byte("fatal finalizer")) { + t.Fatalf("logger dont have fatal message, buf %s", buf.Bytes()) + } + if !bytes.Contains(buf.Bytes(), []byte("info_msg1")) { + t.Fatalf("logger dont have info_msg1 message, buf %s", buf.Bytes()) + } +} diff --git a/logger/unwrap/unwrap.go b/logger/unwrap/unwrap.go index 8bab179d..685ea9e6 100644 --- a/logger/unwrap/unwrap.go +++ b/logger/unwrap/unwrap.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" - "go.unistack.org/micro/v3/codec" + "go.unistack.org/micro/v4/codec" ) const sf = "0-+# " diff --git a/logger/unwrap/unwrap_test.go b/logger/unwrap/unwrap_test.go index af55db0a..2395ecaf 100644 --- a/logger/unwrap/unwrap_test.go +++ b/logger/unwrap/unwrap_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "go.unistack.org/micro/v3/codec" + "go.unistack.org/micro/v4/codec" ) func TestUnwrap(t *testing.T) { diff --git a/meter/meter.go b/meter/meter.go index 90540859..211c6a81 100644 --- a/meter/meter.go +++ b/meter/meter.go @@ -4,8 +4,8 @@ package meter import ( "io" "sort" - "strconv" "strings" + "sync" "time" ) @@ -49,9 +49,11 @@ type Meter interface { Set(opts ...Option) Meter // Histogram get or create histogram Histogram(name string, labels ...string) Histogram + // HistogramExt get or create histogram with specified quantiles + HistogramExt(name string, quantiles []float64, labels ...string) Histogram // Summary get or create summary Summary(name string, labels ...string) Summary - // SummaryExt get or create summary with spcified quantiles and window time + // SummaryExt get or create summary with specified quantiles and window time SummaryExt(name string, window time.Duration, quantiles []float64, labels ...string) Summary // Write writes metrics to io.Writer Write(w io.Writer, opts ...Option) error @@ -59,6 +61,8 @@ type Meter interface { Options() Options // String return meter type String() string + // Unregister metric name and drop all data + Unregister(name string, labels ...string) bool } // Counter is a counter @@ -80,7 +84,11 @@ type FloatCounter interface { // Gauge is a float64 gauge type Gauge interface { + Add(float64) Get() float64 + Set(float64) + Dec() + Inc() } // Histogram is a histogram for non-negative values with automatically created buckets @@ -117,6 +125,39 @@ func BuildLabels(labels ...string) []string { return labels } +var spool = newStringsPool(500) + +type stringsPool struct { + p *sync.Pool + c int +} + +func newStringsPool(size int) *stringsPool { + p := &stringsPool{c: size} + p.p = &sync.Pool{ + New: func() interface{} { + return &strings.Builder{} + }, + } + return p +} + +func (p *stringsPool) Cap() int { + return p.c +} + +func (p *stringsPool) Get() *strings.Builder { + return p.p.Get().(*strings.Builder) +} + +func (p *stringsPool) Put(b *strings.Builder) { + if b.Cap() > p.c { + return + } + b.Reset() + p.p.Put(b) +} + // BuildName used to combine metric with labels. // If labels count is odd, drop last element func BuildName(name string, labels ...string) string { @@ -125,8 +166,6 @@ func BuildName(name string, labels ...string) string { } if len(labels) > 2 { - sort.Sort(byKey(labels)) - idx := 0 for { if labels[idx] == labels[idx+2] { @@ -141,7 +180,9 @@ func BuildName(name string, labels ...string) string { } } - var b strings.Builder + b := spool.Get() + defer spool.Put(b) + _, _ = b.WriteString(name) _, _ = b.WriteRune('{') for idx := 0; idx < len(labels); idx += 2 { @@ -149,8 +190,9 @@ func BuildName(name string, labels ...string) string { _, _ = b.WriteRune(',') } _, _ = b.WriteString(labels[idx]) - _, _ = b.WriteString(`=`) - _, _ = b.WriteString(strconv.Quote(labels[idx+1])) + _, _ = b.WriteString(`="`) + _, _ = b.WriteString(labels[idx+1]) + _, _ = b.WriteRune('"') } _, _ = b.WriteRune('}') diff --git a/meter/meter_test.go b/meter/meter_test.go index df4f01b4..9e9523d7 100644 --- a/meter/meter_test.go +++ b/meter/meter_test.go @@ -50,11 +50,12 @@ func TestBuildName(t *testing.T) { data := map[string][]string{ `my_metric{firstlabel="value2",zerolabel="value3"}`: { "my_metric", - "zerolabel", "value3", "firstlabel", "value2", + "firstlabel", "value2", + "zerolabel", "value3", }, `my_metric{broker="broker2",register="mdns",server="tcp"}`: { "my_metric", - "broker", "broker1", "broker", "broker2", "server", "http", "server", "tcp", "register", "mdns", + "broker", "broker1", "broker", "broker2", "register", "mdns", "server", "http", "server", "tcp", }, `my_metric{aaa="aaa"}`: { "my_metric", diff --git a/meter/noop.go b/meter/noop.go index a65c558b..57340044 100644 --- a/meter/noop.go +++ b/meter/noop.go @@ -28,6 +28,10 @@ func (r *noopMeter) Name() string { return r.opts.Name } +func (r *noopMeter) Unregister(name string, labels ...string) bool { + return true +} + // Init initialize options func (r *noopMeter) Init(opts ...Option) error { for _, o := range opts { @@ -66,6 +70,11 @@ func (r *noopMeter) Histogram(_ string, labels ...string) Histogram { return &noopHistogram{labels: labels} } +// HistogramExt implements the Meter interface +func (r *noopMeter) HistogramExt(_ string, quantiles []float64, labels ...string) Histogram { + return &noopHistogram{labels: labels} +} + // Set implements the Meter interface func (r *noopMeter) Set(opts ...Option) Meter { m := &noopMeter{opts: r.opts} @@ -132,6 +141,18 @@ type noopGauge struct { labels []string } +func (r *noopGauge) Add(float64) { +} + +func (r *noopGauge) Set(float64) { +} + +func (r *noopGauge) Inc() { +} + +func (r *noopGauge) Dec() { +} + func (r *noopGauge) Get() float64 { return 0 } diff --git a/meter/options.go b/meter/options.go index 236bebd3..4a648e27 100644 --- a/meter/options.go +++ b/meter/options.go @@ -4,6 +4,8 @@ import ( "context" ) +var DefaultQuantiles = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} + // Option powers the configuration for metrics implementations: type Option func(*Options) @@ -23,6 +25,8 @@ type Options struct { WriteProcessMetrics bool // WriteFDMetrics flag to write fd metrics WriteFDMetrics bool + // Quantiles specifies buckets for histogram + Quantiles []float64 } // NewOptions prepares a set of options: @@ -61,14 +65,12 @@ func Address(value string) Option { } } -/* -// TimingObjectives defines the desired spread of statistics for histogram / timing metrics: -func TimingObjectives(value map[float64]float64) Option { +// Quantiles defines the desired spread of statistics for histogram metrics: +func Quantiles(quantiles []float64) Option { return func(o *Options) { - o.TimingObjectives = value + o.Quantiles = quantiles } } -*/ // Labels add the meter labels func Labels(ls ...string) Option {