├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cliff.toml ├── examples └── simple │ └── main.go ├── ft.go ├── ft_test.go ├── global.go ├── go.mod └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.3.0] - 2025-03-01 6 | 7 | ### 🐛 Bug Fixes 8 | 9 | - *(test)* Wrong expected duration value 10 | - Wrong duration key in the logs 11 | 12 | ### 🚜 Refactor 13 | 14 | - Improve attribute handling, metric recording, and add documentation 15 | 16 | ### 🧪 Testing 17 | 18 | - Update duration attribute and metric unit tests to match implementation 19 | 20 | ## [0.2.1] - 2025-01-28 21 | 22 | ### 🐛 Bug Fixes 23 | 24 | - Incorrect const values 25 | 26 | ### ⚙️ Miscellaneous Tasks 27 | 28 | - Add git-cliff configuration for changelog generation 29 | - Add changelog 30 | - Update example formatting in readme 31 | - Update simple example 32 | - Update readme 33 | - Update readme 34 | - Fix examples in readme 35 | - Update changelog 36 | 37 | ## [0.2.0] - 2025-01-25 38 | 39 | ### 🚀 Features 40 | 41 | - Add test dependencies and OpenTelemetry SDK to go.mod 42 | 43 | ### 🚜 Refactor 44 | 45 | - Reorganize global config and improve tracing 46 | - Resolve slog value before converting to string 47 | - Clean up test code and add attribute type tests 48 | - Remove unused log level getter functions 49 | 50 | ### 🧪 Testing 51 | 52 | - Add comprehensive test suite for ft package functionality 53 | - Add comprehensive test suite for ft package 54 | 55 | ### ⚙️ Miscellaneous Tasks 56 | 57 | - Update dependencies and upgrade Go version to 1.23.5 58 | - Update go mods 59 | - Update readme 60 | 61 | ## [0.1.0] - 2024-01-18 62 | 63 | ### 🐛 Bug Fixes 64 | 65 | - Wrong log level on error 66 | 67 | 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD Zero Clause License 2 | 3 | Copyright (c) [2024] [Amanbolat Balabekov] 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ft – function trace 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/amanbolat/ft)](https://goreportcard.com/report/github.com/amanbolat/ft) 4 | [![GoDoc](https://godoc.org/github.com/amanbolat/ft?status.svg)](https://godoc.org/github.com/amanbolat/ft) 5 | [![License](https://img.shields.io/badge/license-BSD%20Zero%20Clause%20License-blue.svg)](https://opensource.org/license/0bsd/) 6 | 7 | A lightweight library for tracing function execution with OpenTelemetry integration, 8 | structured logging, and metrics collection. 9 | 10 | ## Why? 11 | 12 | In most of my projects, I start by relying on simple logs to get quick insights into the application. 13 | As the project evolves, I usually add metrics and traces for improved observability, 14 | but setting up all the libraries can be time-consuming. 15 | This library allows you to enable the observability of your functions with just two lines of code. 16 | 17 | ## Features 18 | 19 | - Structured logging using `slog`. 20 | - OpenTelemetry tracing integration. 21 | - Metrics for execution counts and duration. 22 | - Configurable log level. 23 | - Option to opt out of metrics and tracing. 24 | 25 | ## Usage 26 | 27 | Just add two lines to the beginning of the function: 28 | 29 | ```go 30 | func Do(ctx context.Context) (err error) { 31 | ctx, span := ft.Start(ctx, "main.Do", ft.WithErr(&err)) // Log when we enter the `Do` function. 32 | defer span.End() // Log, trace and meter when we exit the `Do` function. 33 | 34 | err = errors.New("unexpected error") 35 | 36 | return 37 | } 38 | ``` 39 | 40 | If you run the code above, you will see the following output: 41 | 42 | ```shell 43 | time=2025-01-25T21:55:27.068+01:00 level=INFO msg="action started" action=main.Do 44 | time=2025-01-25T21:55:27.069+01:00 level=ERROR msg="action ended" action=main.Do duration_ms=0.743 error="unexpected error" 45 | ``` 46 | 47 | Setup OTEL tracer and meter globally and `ft` will start sending metrics and traces to the OTLP collector: 48 | 49 | ```go 50 | mp, _, _ := autometer.NewMeterProvider(context.Background()) 51 | otel.SetMeterProvider(mp) 52 | tp, _, _ := autotracer.NewTracerProvider(context.Background()) 53 | otel.SetTracerProvider(tp) 54 | 55 | ft.SetMetricsEnabled(true) 56 | ft.SetTracingEnabled(true) 57 | ``` 58 | 59 | > [!TIP] 60 | > In the example above I use [go-faster/sdk](https://github.com/go-faster/sdk) to setup OTEL based on environment 61 | > variables. 62 | 63 | 64 | ## Configuration 65 | 66 | `ft` package provides many functions to configure its behaviour. See the table below: 67 | 68 | Here's a markdown table with all the `Set` functions and their descriptions: 69 | 70 | | Function | Description | 71 | |------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| 72 | | `SetDurationMetricUnit(unit string)` | Sets the global duration metric unit. Accepts either millisecond (`ms`) or second (`s`) as valid units. Defaults to millisecond if invalid unit is provided. | 73 | | `SetDefaultLogger(l *slog.Logger)` | Sets the global logger instance. Does nothing if nil logger is provided. | 74 | | `SetLogLevelOnFailure(level slog.Level)` | Sets the global log level for failure scenarios. | 75 | | `SetLogLevelOnSuccess(level slog.Level)` | Sets the global log level for success scenarios. | 76 | | `SetTracingEnabled(v bool)` | Enables or disables global tracing functionality. | 77 | | `SetMetricsEnabled(v bool)` | Enables or disables global metrics collection. | 78 | | `SetClock(c clockwork.Clock)` | Sets the global clock instance used for time-related operations. | 79 | | `SetAppendOtelAttrs(v bool)` | Enables or disables the appending of OpenTelemetry attributes globally. | 80 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ default configuration file 2 | # https://git-cliff.org/docs/configuration 3 | # 4 | # Lines starting with "#" are comments. 5 | # Configuration options are organized into tables and keys. 6 | # See documentation for more information on available options. 7 | 8 | [changelog] 9 | # template for the changelog header 10 | header = """ 11 | # Changelog\n 12 | All notable changes to this project will be documented in this file.\n 13 | """ 14 | # template for the changelog body 15 | # https://keats.github.io/tera/docs/#introduction 16 | body = """ 17 | {% if version %}\ 18 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 19 | {% else %}\ 20 | ## [unreleased] 21 | {% endif %}\ 22 | {% for group, commits in commits | group_by(attribute="group") %} 23 | ### {{ group | striptags | trim | upper_first }} 24 | {% for commit in commits %} 25 | - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ 26 | {% if commit.breaking %}[**breaking**] {% endif %}\ 27 | {{ commit.message | upper_first }}\ 28 | {% endfor %} 29 | {% endfor %}\n 30 | """ 31 | # template for the changelog footer 32 | footer = """ 33 | 34 | """ 35 | # remove the leading and trailing s 36 | trim = true 37 | # postprocessors 38 | postprocessors = [ 39 | # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL 40 | ] 41 | # render body even when there are no releases to process 42 | # render_always = true 43 | # output file path 44 | # output = "test.md" 45 | 46 | [git] 47 | # parse the commits based on https://www.conventionalcommits.org 48 | conventional_commits = true 49 | # filter out the commits that are not conventional 50 | filter_unconventional = true 51 | # process each line of a commit as an individual commit 52 | split_commits = false 53 | # regex for preprocessing the commit messages 54 | commit_preprocessors = [ 55 | # Replace issue numbers 56 | #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, 57 | # Check spelling of the commit with https://github.com/crate-ci/typos 58 | # If the spelling is incorrect, it will be automatically fixed. 59 | #{ pattern = '.*', replace_command = 'typos --write-changes -' }, 60 | ] 61 | # regex for parsing and grouping commits 62 | commit_parsers = [ 63 | { message = "^feat", group = "🚀 Features" }, 64 | { message = "^fix", group = "🐛 Bug Fixes" }, 65 | { message = "^doc", group = "📚 Documentation" }, 66 | { message = "^perf", group = "⚡ Performance" }, 67 | { message = "^refactor", group = "🚜 Refactor" }, 68 | { message = "^style", group = "🎨 Styling" }, 69 | { message = "^test", group = "🧪 Testing" }, 70 | { message = "^chore\\(release\\): prepare for", skip = true }, 71 | { message = "^chore\\(deps.*\\)", skip = true }, 72 | { message = "^chore\\(pr\\)", skip = true }, 73 | { message = "^chore\\(pull\\)", skip = true }, 74 | { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks" }, 75 | { body = ".*security", group = "🛡️ Security" }, 76 | { message = "^revert", group = "◀️ Revert" }, 77 | { message = ".*", group = "💼 Other" }, 78 | ] 79 | # filter out the commits that are not matched by commit parsers 80 | filter_commits = false 81 | # sort the tags topologically 82 | topo_order = false 83 | # sort the commits inside sections by oldest/newest order 84 | sort_commits = "oldest" 85 | -------------------------------------------------------------------------------- /examples/simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/amanbolat/ft" 8 | ) 9 | 10 | // Running this example will produce an output similar to this: 11 | // 12 | // time=2025-01-25T21:55:27.068+01:00 level=INFO msg="action started" action=main.Do 13 | // time=2025-01-25T21:55:27.069+01:00 level=ERROR msg="action ended" action=main.Do duration_ms=0.743 error="unexpected error" 14 | func main() { 15 | ctx := context.Background() 16 | _ = Do(ctx) 17 | } 18 | 19 | func Do(ctx context.Context) (err error) { 20 | ctx, span := ft.Start(ctx, "main.Do", ft.WithErr(&err)) 21 | defer span.End() 22 | 23 | err = errors.New("unexpected error") 24 | 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /ft.go: -------------------------------------------------------------------------------- 1 | package ft 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "runtime" 7 | 8 | "github.com/puzpuzpuz/xsync/v3" 9 | "go.opentelemetry.io/otel" 10 | "go.opentelemetry.io/otel/attribute" 11 | "go.opentelemetry.io/otel/codes" 12 | "go.opentelemetry.io/otel/metric" 13 | semconv "go.opentelemetry.io/otel/semconv/v1.21.0" 14 | "go.opentelemetry.io/otel/trace" 15 | 16 | "log/slog" 17 | "time" 18 | ) 19 | 20 | const ( 21 | instrumentationName = "github.com/amanbolat/ft" 22 | // DurationMetricUnitSecond represents seconds as the unit for duration metrics 23 | DurationMetricUnitSecond = "s" 24 | // DurationMetricUnitMillisecond represents milliseconds as the unit for duration metrics 25 | DurationMetricUnitMillisecond = "ms" 26 | ) 27 | 28 | var int64Counters = xsync.NewMapOf[string, metric.Int64Counter]() 29 | var durationHistograms = xsync.NewMapOf[string, metric.Float64Histogram]() 30 | 31 | type SpanConfig struct { 32 | err *error 33 | additionalAttrs []slog.Attr 34 | } 35 | 36 | type Option func(cfg *SpanConfig) 37 | 38 | func WithErr(err *error) Option { 39 | return func(cfg *SpanConfig) { 40 | cfg.err = err 41 | } 42 | } 43 | 44 | func WithAttrs(attrs ...slog.Attr) Option { 45 | return func(cfg *SpanConfig) { 46 | cfg.additionalAttrs = append(cfg.additionalAttrs, attrs...) 47 | } 48 | } 49 | 50 | // Span represents a traced and logged operation that can be ended 51 | type Span interface { 52 | End() 53 | } 54 | 55 | type span struct { 56 | ctx context.Context 57 | start time.Time 58 | action string 59 | traceSpan trace.Span 60 | err *error 61 | additionalAttrs []slog.Attr 62 | } 63 | 64 | // Start begins a new traced and logged span for the given action. 65 | // It returns an updated context and a Span that should be ended when the operation completes. 66 | func Start(ctx context.Context, action string, opts ...Option) (context.Context, Span) { 67 | now := (*globalClock.Load()).Now() 68 | 69 | cfg := &SpanConfig{} 70 | 71 | for _, opt := range opts { 72 | opt(cfg) 73 | } 74 | 75 | if ctx == nil { 76 | ctx = context.Background() 77 | } 78 | var otelSpan trace.Span 79 | 80 | if globalTracingEnabled.Load() { 81 | ctx, otelSpan = otel.Tracer( 82 | instrumentationName, 83 | trace.WithSchemaURL(semconv.SchemaURL), 84 | ).Start( 85 | ctx, 86 | action, 87 | trace.WithSpanKind(trace.SpanKindInternal), 88 | trace.WithAttributes( 89 | attribute.String("action", action), 90 | ), 91 | trace.WithTimestamp(now), 92 | ) 93 | } 94 | 95 | if otelSpan != nil && otelSpan.IsRecording() && globalAppendOtelAttrs.Load() && len(cfg.additionalAttrs) > 0 { 96 | otelAttrs := make([]attribute.KeyValue, 0, len(cfg.additionalAttrs)) 97 | for _, attr := range cfg.additionalAttrs { 98 | otelAttrs = append(otelAttrs, mapSlogAttrToOtel(attr)) 99 | } 100 | otelSpan.SetAttributes(otelAttrs...) 101 | } 102 | 103 | if globalMetricsEnabled.Load() { 104 | metricName := action + "_counter" 105 | counter, ok := int64Counters.Load(metricName) 106 | 107 | if !ok { 108 | var err error 109 | counter, err = otel.GetMeterProvider().Meter(instrumentationName).Int64Counter(metricName) 110 | if err == nil { 111 | int64Counters.Store(metricName, counter) 112 | ok = true 113 | } 114 | } 115 | 116 | if ok { 117 | counter.Add(ctx, 1) 118 | } 119 | } 120 | 121 | attrs := make([]slog.Attr, 0, 1+len(cfg.additionalAttrs)) 122 | attrs = append(attrs, slog.String("action", action)) 123 | attrs = append(attrs, cfg.additionalAttrs...) 124 | 125 | log(ctx, "action started", globalLogLevelEndOnSuccess.Level(), now, attrs...) 126 | 127 | return ctx, span{ 128 | ctx: ctx, 129 | start: now, 130 | action: action, 131 | traceSpan: otelSpan, 132 | err: cfg.err, 133 | additionalAttrs: cfg.additionalAttrs, 134 | } 135 | } 136 | 137 | func (s span) End() { 138 | if s.ctx == nil { 139 | s.ctx = context.Background() 140 | } 141 | now := (*globalClock.Load()).Now() 142 | duration := now.Sub(s.start) 143 | level := globalLogLevelEndOnSuccess.Level() 144 | 145 | durationMetricSuffix := "_duration_milliseconds" 146 | durationAttrKey := "duration_ms" 147 | durationAttrVal := durationToMillisecond(duration) 148 | 149 | durationMetricUnit := globalDurationMetricUnit.Load() 150 | 151 | if durationMetricUnit == DurationMetricUnitSecond { 152 | durationAttrKey = "duration_s" 153 | durationAttrVal = durationToSecond(duration) 154 | durationMetricSuffix = "_duration_seconds" 155 | } 156 | 157 | attrs := make([]slog.Attr, 0, 2+len(s.additionalAttrs)) 158 | attrs = append(attrs, slog.String("action", s.action), slog.Float64(durationAttrKey, durationAttrVal)) 159 | attrs = append(attrs, s.additionalAttrs...) 160 | 161 | if s.err != nil && *s.err != nil { 162 | level = globalLogLevelEndOnFailure.Level() 163 | attrs = append(attrs, slog.Any("error", *s.err)) 164 | 165 | if s.traceSpan != nil { 166 | s.traceSpan.RecordError(*s.err, trace.WithStackTrace(true)) 167 | s.traceSpan.SetStatus(codes.Error, (*s.err).Error()) 168 | } 169 | } 170 | 171 | if globalMetricsEnabled.Load() { 172 | metricName := s.action + durationMetricSuffix 173 | histogram, ok := durationHistograms.Load(metricName) 174 | 175 | if !ok { 176 | var err error 177 | histogram, err = otel.GetMeterProvider(). 178 | Meter(instrumentationName). 179 | Float64Histogram( 180 | metricName, 181 | metric.WithUnit(durationMetricUnit), 182 | metric.WithDescription(fmt.Sprintf("[%s] action duration", s.action)), 183 | ) 184 | if err == nil { 185 | durationHistograms.Store(metricName, histogram) 186 | ok = true 187 | } 188 | } 189 | 190 | if ok { 191 | if durationMetricUnit == DurationMetricUnitSecond { 192 | histogram.Record(s.ctx, duration.Seconds()) 193 | } else { 194 | histogram.Record(s.ctx, float64(duration.Milliseconds())/1000) 195 | } 196 | } 197 | } 198 | 199 | log(s.ctx, "action ended", level, now, attrs...) 200 | 201 | if s.traceSpan != nil { 202 | s.traceSpan.End(trace.WithTimestamp(now)) 203 | } 204 | } 205 | 206 | func log(ctx context.Context, msg string, level slog.Level, now time.Time, attrs ...slog.Attr) { 207 | var pcs [1]uintptr 208 | runtime.Callers(3, pcs[:]) 209 | r := slog.NewRecord(now, level, msg, pcs[0]) 210 | r.AddAttrs(attrs...) 211 | _ = globalLogger.Load().Handler().Handle(ctx, r) 212 | } 213 | 214 | // mapSlogAttrToOtel converts a slog.Attr to an OpenTelemetry attribute.KeyValue 215 | func mapSlogAttrToOtel(v slog.Attr) attribute.KeyValue { 216 | key := v.Key 217 | value := v.Value 218 | 219 | switch value.Kind() { 220 | case slog.KindBool: 221 | return attribute.Bool(key, value.Bool()) 222 | case slog.KindDuration: 223 | return attribute.Int64(key, int64(value.Duration())) 224 | case slog.KindFloat64: 225 | return attribute.Float64(key, value.Float64()) 226 | case slog.KindInt64: 227 | return attribute.Int64(key, value.Int64()) 228 | case slog.KindString: 229 | return attribute.String(key, value.String()) 230 | case slog.KindTime: 231 | return attribute.String(key, value.Time().Format(time.RFC3339)) 232 | case slog.KindGroup: 233 | return attribute.String(key, fmt.Sprintf("%v", value.Group())) 234 | default: 235 | return attribute.String(key, value.Resolve().String()) 236 | } 237 | } 238 | 239 | func durationToMillisecond(d time.Duration) float64 { 240 | return float64(d/1000) / 1000 241 | } 242 | 243 | func durationToSecond(d time.Duration) float64 { 244 | return float64(d/1000) / 1000000 245 | } 246 | -------------------------------------------------------------------------------- /ft_test.go: -------------------------------------------------------------------------------- 1 | package ft_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "log/slog" 8 | "testing" 9 | "time" 10 | 11 | "github.com/amanbolat/ft" 12 | "github.com/jonboulle/clockwork" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | "go.opentelemetry.io/otel" 16 | "go.opentelemetry.io/otel/attribute" 17 | "go.opentelemetry.io/otel/codes" 18 | "go.opentelemetry.io/otel/metric/noop" 19 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 20 | "go.opentelemetry.io/otel/sdk/trace/tracetest" 21 | ) 22 | 23 | func TestSpan_Basic(t *testing.T) { 24 | ft.SetDefaultLogger(slog.New(slog.NewTextHandler(io.Discard, nil))) 25 | spanRecorder := tracetest.NewSpanRecorder() 26 | tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(spanRecorder)) 27 | otel.SetTracerProvider(tp) 28 | 29 | fakeClock := clockwork.NewFakeClock() 30 | ft.SetClock(fakeClock) 31 | 32 | ft.SetTracingEnabled(true) 33 | 34 | ctx := context.Background() 35 | testAction := "test_action" 36 | 37 | ctx, span := ft.Start(ctx, testAction) 38 | fakeClock.Advance(100 * time.Millisecond) 39 | span.End() 40 | 41 | spans := spanRecorder.Ended() 42 | require.Len(t, spans, 1) 43 | 44 | recordedSpan := spans[0] 45 | assert.Equal(t, testAction, recordedSpan.Name()) 46 | assert.Equal(t, codes.Unset, recordedSpan.Status().Code) 47 | assert.Equal(t, 100*time.Millisecond, recordedSpan.EndTime().Sub(recordedSpan.StartTime())) 48 | } 49 | 50 | func TestSpan_WithError(t *testing.T) { 51 | ft.SetDefaultLogger(slog.New(slog.NewTextHandler(io.Discard, nil))) 52 | spanRecorder := tracetest.NewSpanRecorder() 53 | tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(spanRecorder)) 54 | otel.SetTracerProvider(tp) 55 | 56 | fakeClock := clockwork.NewFakeClock() 57 | ft.SetClock(fakeClock) 58 | ft.SetTracingEnabled(true) 59 | 60 | ctx := context.Background() 61 | testAction := "test_action_error" 62 | testError := errors.New("test error") 63 | var err error = testError 64 | 65 | ctx, span := ft.Start(ctx, testAction, ft.WithErr(&err)) 66 | fakeClock.Advance(50 * time.Millisecond) 67 | span.End() 68 | 69 | spans := spanRecorder.Ended() 70 | require.Len(t, spans, 1) 71 | 72 | recordedSpan := spans[0] 73 | assert.Equal(t, testAction, recordedSpan.Name()) 74 | assert.Equal(t, codes.Error, recordedSpan.Status().Code) 75 | assert.Equal(t, testError.Error(), recordedSpan.Status().Description) 76 | } 77 | 78 | type CustomValue struct { 79 | val1 string 80 | val2 string 81 | } 82 | 83 | func (v CustomValue) LogValue() slog.Value { 84 | return slog.StringValue(v.val1 + "_" + v.val2) 85 | } 86 | 87 | func TestSpan_WithAttributes(t *testing.T) { 88 | ft.SetDefaultLogger(slog.New(slog.NewTextHandler(io.Discard, nil))) 89 | spanRecorder := tracetest.NewSpanRecorder() 90 | tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(spanRecorder)) 91 | otel.SetTracerProvider(tp) 92 | 93 | fakeClock := clockwork.NewFakeClock() 94 | ft.SetClock(fakeClock) 95 | ft.SetTracingEnabled(true) 96 | ft.SetAppendOtelAttrs(true) 97 | 98 | ctx := context.Background() 99 | testAction := "test_action_attrs" 100 | 101 | ctx, span := ft.Start(ctx, testAction, ft.WithAttrs( 102 | slog.String("string", "value"), 103 | slog.Int64("int", 1), 104 | slog.Bool("bool", true), 105 | slog.Duration("custom_duration", time.Second*500), 106 | slog.Float64("float", 2), 107 | slog.Time("timestamp", time.Date(2024, 1, 1, 1, 1, 1, 1, time.UTC)), 108 | slog.Group("group", slog.String("string", "value"), slog.Int64("int", 1)), 109 | slog.Any("custom_value", CustomValue{val1: "a", val2: "b"}), 110 | )) 111 | 112 | span.End() 113 | 114 | spans := spanRecorder.Ended() 115 | require.Len(t, spans, 1) 116 | 117 | recordedSpan := spans[0] 118 | assert.Equal(t, testAction, recordedSpan.Name()) 119 | 120 | expectedAttrs := []attribute.KeyValue{ 121 | attribute.String("action", testAction), 122 | attribute.String("string", "value"), 123 | attribute.Int64("int", 1), 124 | attribute.Bool("bool", true), 125 | attribute.Int64("custom_duration", int64(time.Second*500)), 126 | attribute.Float64("float", 2), 127 | attribute.String("timestamp", time.Date(2024, 1, 1, 1, 1, 1, 1, time.UTC).Format(time.RFC3339)), 128 | attribute.String("group", "[string=value int=1]"), 129 | attribute.String("custom_value", "a_b"), 130 | } 131 | 132 | spanAttrs := recordedSpan.Attributes() 133 | assert.ElementsMatch(t, expectedAttrs, spanAttrs) 134 | } 135 | 136 | func TestSpan_TracingDisabled(t *testing.T) { 137 | ft.SetDefaultLogger(slog.New(slog.NewTextHandler(io.Discard, nil))) 138 | spanRecorder := tracetest.NewSpanRecorder() 139 | tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(spanRecorder)) 140 | otel.SetTracerProvider(tp) 141 | 142 | fakeClock := clockwork.NewFakeClock() 143 | ft.SetClock(fakeClock) 144 | ft.SetTracingEnabled(false) 145 | 146 | ctx := context.Background() 147 | testAction := "test_action_disabled" 148 | 149 | ctx, span := ft.Start(ctx, testAction) 150 | span.End() 151 | 152 | spans := spanRecorder.Ended() 153 | assert.Empty(t, spans) 154 | } 155 | 156 | func TestSpan_MetricsEnabled(t *testing.T) { 157 | ft.SetDefaultLogger(slog.New(slog.NewTextHandler(io.Discard, nil))) 158 | fakeClock := clockwork.NewFakeClock() 159 | ft.SetClock(fakeClock) 160 | 161 | ft.SetMetricsEnabled(true) 162 | ft.SetDurationMetricUnit(ft.DurationMetricUnitMillisecond) 163 | 164 | mp := noop.NewMeterProvider() 165 | otel.SetMeterProvider(mp) 166 | 167 | ctx := context.Background() 168 | testAction := "test_action_metrics" 169 | 170 | ctx, span := ft.Start(ctx, testAction) 171 | fakeClock.Advance(75 * time.Millisecond) 172 | span.End() 173 | } 174 | 175 | func TestSpan_NilContext(t *testing.T) { 176 | ft.SetDefaultLogger(slog.New(slog.NewTextHandler(io.Discard, nil))) 177 | spanRecorder := tracetest.NewSpanRecorder() 178 | tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(spanRecorder)) 179 | otel.SetTracerProvider(tp) 180 | 181 | fakeClock := clockwork.NewFakeClock() 182 | ft.SetClock(fakeClock) 183 | ft.SetTracingEnabled(true) 184 | 185 | testAction := "test_action_nil_ctx" 186 | ctx, span := ft.Start(nil, testAction) 187 | assert.NotNil(t, ctx) 188 | 189 | span.End() 190 | 191 | spans := spanRecorder.Ended() 192 | require.Len(t, spans, 1) 193 | assert.Equal(t, testAction, spans[0].Name()) 194 | } 195 | 196 | func TestSpan_DurationUnits(t *testing.T) { 197 | ft.SetDefaultLogger(slog.New(slog.NewTextHandler(io.Discard, nil))) 198 | fakeClock := clockwork.NewFakeClock() 199 | ft.SetClock(fakeClock) 200 | ft.SetMetricsEnabled(true) 201 | 202 | ft.SetDurationMetricUnit(ft.DurationMetricUnitMillisecond) 203 | var logBuffer testLogBuffer 204 | logger := slog.New(slog.NewTextHandler(&logBuffer, nil)) 205 | ft.SetDefaultLogger(logger) 206 | 207 | _, span := ft.Start(context.Background(), "test_ms") 208 | fakeClock.Advance(100 * time.Millisecond) 209 | span.End() 210 | 211 | assert.Contains(t, logBuffer.String(), "duration_ms=") 212 | 213 | logBuffer.Reset() 214 | ft.SetDurationMetricUnit(ft.DurationMetricUnitSecond) 215 | _, span = ft.Start(context.Background(), "test_s") 216 | fakeClock.Advance(1 * time.Second) 217 | span.End() 218 | 219 | assert.Contains(t, logBuffer.String(), "duration_s=") 220 | } 221 | 222 | func TestSpan_LogLevels(t *testing.T) { 223 | fakeClock := clockwork.NewFakeClock() 224 | ft.SetClock(fakeClock) 225 | 226 | var logBuffer testLogBuffer 227 | logger := slog.New(slog.NewTextHandler(&logBuffer, nil)) 228 | ft.SetDefaultLogger(logger) 229 | 230 | ft.SetLogLevelOnSuccess(slog.LevelDebug) 231 | ft.SetLogLevelOnFailure(slog.LevelError) 232 | 233 | _, span := ft.Start(context.Background(), "test_success") 234 | span.End() 235 | assert.Contains(t, logBuffer.String(), "level=DEBUG") 236 | 237 | logBuffer.Reset() 238 | err := errors.New("test error") 239 | _, span = ft.Start(context.Background(), "test_failure", ft.WithErr(&err)) 240 | span.End() 241 | assert.Contains(t, logBuffer.String(), "level=ERROR") 242 | } 243 | 244 | type testLogBuffer struct { 245 | content string 246 | } 247 | 248 | func (b *testLogBuffer) Write(p []byte) (n int, err error) { 249 | b.content += string(p) 250 | return len(p), nil 251 | } 252 | 253 | func (b *testLogBuffer) String() string { 254 | return b.content 255 | } 256 | 257 | func (b *testLogBuffer) Reset() { 258 | b.content = "" 259 | } 260 | -------------------------------------------------------------------------------- /global.go: -------------------------------------------------------------------------------- 1 | package ft 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | 7 | "github.com/jonboulle/clockwork" 8 | "github.com/samber/lo" 9 | "go.uber.org/atomic" 10 | ) 11 | 12 | var ( 13 | globalLogger = atomic.NewPointer(slog.New(slog.NewTextHandler(os.Stdout, nil))) 14 | globalTracingEnabled = atomic.NewBool(false) 15 | globalMetricsEnabled = atomic.NewBool(false) 16 | globalAppendOtelAttrs = atomic.NewBool(false) 17 | globalDurationMetricUnit = atomic.NewString(DurationMetricUnitMillisecond) 18 | globalClock = atomic.NewPointer[clockwork.Clock](lo.ToPtr(clockwork.NewRealClock())) 19 | 20 | globalLogLevelEndOnSuccess slog.LevelVar 21 | globalLogLevelEndOnFailure slog.LevelVar 22 | ) 23 | 24 | func init() { 25 | globalLogLevelEndOnSuccess.Set(slog.LevelInfo) 26 | globalLogLevelEndOnFailure.Set(slog.LevelError) 27 | } 28 | 29 | func SetDurationMetricUnit(unit string) { 30 | switch unit { 31 | case DurationMetricUnitMillisecond, DurationMetricUnitSecond: 32 | globalDurationMetricUnit.Store(unit) 33 | default: 34 | globalDurationMetricUnit.Store(DurationMetricUnitMillisecond) 35 | } 36 | } 37 | 38 | func SetDefaultLogger(l *slog.Logger) { 39 | if l == nil { 40 | return 41 | } 42 | 43 | globalLogger.Store(l) 44 | } 45 | 46 | func SetLogLevelOnFailure(level slog.Level) { 47 | globalLogLevelEndOnFailure.Set(level) 48 | } 49 | 50 | func SetLogLevelOnSuccess(level slog.Level) { 51 | globalLogLevelEndOnSuccess.Set(level) 52 | } 53 | 54 | func SetTracingEnabled(v bool) { 55 | globalTracingEnabled.Store(v) 56 | } 57 | 58 | func SetMetricsEnabled(v bool) { 59 | globalMetricsEnabled.Store(v) 60 | } 61 | 62 | func SetClock(c clockwork.Clock) { 63 | globalClock.Store(&c) 64 | } 65 | 66 | func SetAppendOtelAttrs(v bool) { 67 | globalAppendOtelAttrs.Store(v) 68 | } 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/amanbolat/ft 2 | 3 | go 1.23.5 4 | 5 | require ( 6 | github.com/go-faster/sdk v0.18.0 7 | github.com/jonboulle/clockwork v0.5.0 8 | github.com/puzpuzpuz/xsync/v3 v3.4.1 9 | github.com/samber/lo v1.47.0 10 | github.com/stretchr/testify v1.10.0 11 | go.opentelemetry.io/otel v1.34.0 12 | go.opentelemetry.io/otel/metric v1.34.0 13 | go.opentelemetry.io/otel/sdk v1.33.0 14 | go.opentelemetry.io/otel/trace v1.34.0 15 | go.uber.org/atomic v1.11.0 16 | ) 17 | 18 | require ( 19 | github.com/beorn7/perks v1.0.1 // indirect 20 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 21 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/go-faster/errors v0.7.1 // indirect 24 | github.com/go-logr/logr v1.4.2 // indirect 25 | github.com/go-logr/stdr v1.2.2 // indirect 26 | github.com/google/uuid v1.6.0 // indirect 27 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect 28 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 29 | github.com/pmezard/go-difflib v1.0.0 // indirect 30 | github.com/prometheus/client_golang v1.20.5 // indirect 31 | github.com/prometheus/client_model v0.6.1 // indirect 32 | github.com/prometheus/common v0.61.0 // indirect 33 | github.com/prometheus/procfs v0.15.1 // indirect 34 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 35 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 // indirect 36 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 // indirect 37 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect 38 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect 39 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect 40 | go.opentelemetry.io/otel/exporters/prometheus v0.55.0 // indirect 41 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 // indirect 42 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 // indirect 43 | go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect 44 | go.opentelemetry.io/proto/otlp v1.4.0 // indirect 45 | go.uber.org/multierr v1.11.0 // indirect 46 | go.uber.org/zap v1.27.0 // indirect 47 | golang.org/x/net v0.33.0 // indirect 48 | golang.org/x/sys v0.29.0 // indirect 49 | golang.org/x/text v0.21.0 // indirect 50 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect 51 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 52 | google.golang.org/grpc v1.69.2 // indirect 53 | google.golang.org/protobuf v1.35.2 // indirect 54 | gopkg.in/yaml.v3 v3.0.1 // indirect 55 | ) 56 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 4 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= 10 | github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= 11 | github.com/go-faster/sdk v0.18.0 h1:zHwbVfjRO1lkovWxubl75XJdT7hW1mQMrjRtuSgQ0m8= 12 | github.com/go-faster/sdk v0.18.0/go.mod h1:IWvsQ1GvPtmylD6hzo5myM7kpBEQu43gyC3tcbXC3Q8= 13 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 14 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 15 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 16 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 17 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 18 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 19 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 20 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 21 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 22 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 23 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 24 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= 25 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= 26 | github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= 27 | github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= 28 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 29 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 30 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 31 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 32 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 33 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 34 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 35 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 36 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 37 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 38 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 39 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 40 | github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= 41 | github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 42 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 43 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 44 | github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= 45 | github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= 46 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 47 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 48 | github.com/puzpuzpuz/xsync/v3 v3.4.1 h1:wWXLKXwzpsduC3kUSahiL45MWxkGb+AQG0dsri4iftA= 49 | github.com/puzpuzpuz/xsync/v3 v3.4.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= 50 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 51 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 52 | github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= 53 | github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= 54 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 55 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 56 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 57 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 58 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 59 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 60 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA= 61 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE= 62 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 h1:bSjzTvsXZbLSWU8hnZXcKmEVaJjjnandxD0PxThhVU8= 63 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0/go.mod h1:aj2rilHL8WjXY1I5V+ra+z8FELtk681deydgYT8ikxU= 64 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= 65 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= 66 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= 67 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= 68 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= 69 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= 70 | go.opentelemetry.io/otel/exporters/prometheus v0.55.0 h1:sSPw658Lk2NWAv74lkD3B/RSDb+xRFx46GjkrL3VUZo= 71 | go.opentelemetry.io/otel/exporters/prometheus v0.55.0/go.mod h1:nC00vyCmQixoeaxF6KNyP42II/RHa9UdruK02qBmHvI= 72 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 h1:FiOTYABOX4tdzi8A0+mtzcsTmi6WBOxk66u0f1Mj9Gs= 73 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0/go.mod h1:xyo5rS8DgzV0Jtsht+LCEMwyiDbjpsxBpWETwFRF0/4= 74 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 h1:W5AWUn/IVe8RFb5pZx1Uh9Laf/4+Qmm4kJL5zPuvR+0= 75 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0/go.mod h1:mzKxJywMNBdEX8TSJais3NnsVZUaJ+bAy6UxPTng2vk= 76 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 77 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 78 | go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= 79 | go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= 80 | go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU= 81 | go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q= 82 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 83 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 84 | go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= 85 | go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= 86 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 87 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 88 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 89 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 90 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 91 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 92 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 93 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 94 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 95 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 96 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 97 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 98 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 99 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 100 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= 101 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= 102 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= 103 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= 104 | google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= 105 | google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= 106 | google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= 107 | google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 108 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 109 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 110 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 111 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 112 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 113 | --------------------------------------------------------------------------------