├── VERSION ├── CHANGELOG.md ├── client ├── doc.go └── client.go ├── labels ├── doc.go └── labeler.go ├── .pre-commit-config.yaml ├── LICENSE ├── interrupt.go ├── .gitignore ├── go.mod ├── example_test.go ├── interrupt_test.go ├── README.md ├── .editorconfig ├── CONTRIBUTING.md ├── options.go ├── telemetry_test.go ├── Makefile ├── go.sum └── telemetry.go /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.24 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | -------------------------------------------------------------------------------- /client/doc.go: -------------------------------------------------------------------------------- 1 | package client 2 | -------------------------------------------------------------------------------- /labels/doc.go: -------------------------------------------------------------------------------- 1 | package labels 2 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/gitleaks/gitleaks 3 | rev: v8.18.2 4 | hooks: 5 | - id: gitleaks 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 polygun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /interrupt.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | "golang.org/x/term" 14 | ) 15 | 16 | // Interrupt is a graceful interrupt + signal handler for the telemetry pipeline. 17 | func Interrupt(ctx context.Context, cancel context.CancelFunc, shutdown func(context.Context) error) chan os.Signal { 18 | // Listen for syscall signals for process to interrupt/quit. 19 | interrupt := make(chan os.Signal, 1) 20 | signal.Notify(interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) 21 | 22 | go func() { 23 | <-interrupt 24 | 25 | if term.IsTerminal(int(os.Stdout.Fd())) { 26 | fmt.Print("\r") 27 | } 28 | 29 | slog.DebugContext(ctx, "Initializing Telemetry Pipeline Shutdown ...") 30 | 31 | // Shutdown signal with grace period of 30 seconds. 32 | handler, timeout := context.WithTimeout(ctx, 30*time.Second) 33 | defer timeout() 34 | go func() { 35 | <-handler.Done() 36 | if errors.Is(handler.Err(), context.DeadlineExceeded) { 37 | slog.Log(ctx, slog.LevelError, "Graceful Telemetry Pipeline Shutdown Timeout - Forcing an Exit ...") 38 | 39 | os.Exit(124) // For portability, 134 cannot be used. 40 | } 41 | }() 42 | 43 | // Trigger graceful shutdown. 44 | if e := shutdown(handler); e != nil { 45 | slog.ErrorContext(ctx, "Exception During Telemetry Pipeline Shutdown", slog.String("error", e.Error())) 46 | } 47 | 48 | slog.InfoContext(ctx, "Telemetry Pipeline Shutdown Complete") 49 | 50 | cancel() 51 | }() 52 | 53 | return interrupt 54 | } 55 | -------------------------------------------------------------------------------- /labels/labeler.go: -------------------------------------------------------------------------------- 1 | package labels 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 8 | ) 9 | 10 | // Options defines configuration options including logging level settings. 11 | type Options struct { 12 | // Attributes is an optional map[string]string to use when generating the [Labeler] function's [slog.Log]-related [slog.String] message(s). 13 | // 14 | // - The default is an empty map. 15 | Attributes map[string]string 16 | 17 | // Level specifies the logging level for controlling the verbosity of log output in the configuration options. 18 | // 19 | // - The default value is [slog.LevelWarn]. 20 | Level slog.Level 21 | } 22 | 23 | // defaults initializes and returns a default Options instance with predefined configuration settings. 24 | func defaults() *Options { 25 | return &Options{ 26 | Attributes: map[string]string{}, 27 | Level: slog.LevelWarn, 28 | } 29 | } 30 | 31 | // Labeler retrieves an [otelhttp.Labeler] from the given context or logs a message if none exists, using optional configuration [Options]. 32 | func Labeler(ctx context.Context, settings ...func(options *Options)) *otelhttp.Labeler { 33 | // Construct the options configuration. 34 | options := defaults() 35 | for _, setting := range settings { 36 | if setting != nil { 37 | setting(options) 38 | } 39 | } 40 | 41 | labeler, found := otelhttp.LabelerFromContext(ctx) 42 | if !(found) { 43 | attributes := make([]slog.Attr, 0) 44 | for k, v := range options.Attributes { 45 | attributes = append(attributes, slog.String(k, v)) 46 | } 47 | 48 | slog.LogAttrs(ctx, options.Level, "No Labeler Found in Context - Any Labeler Attributes Will be Superfluous", attributes...) 49 | } 50 | 51 | return labeler 52 | } 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ############################################################################## 2 | # User-Defined Repository File 3 | # ############################################################################## 4 | 5 | ### Exclusion(s) 6 | 7 | .netrc 8 | .git-credentials 9 | 10 | *.binary 11 | **/*.binary 12 | 13 | *.test 14 | **/*.test 15 | 16 | # ############################################################################## 17 | # Git-Ignore Templates (Auto-Generated, DO NOT MODIFY SECTIONS BELOW) 18 | # ############################################################################## 19 | 20 | ### Jetbrains Template 21 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 22 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 23 | 24 | .idea 25 | 26 | # User-specific stuff 27 | .idea/**/workspace.xml 28 | .idea/**/tasks.xml 29 | .idea/**/usage.statistics.xml 30 | .idea/**/dictionaries 31 | .idea/**/shelf 32 | 33 | # AWS User-specific 34 | .idea/**/aws.xml 35 | 36 | # Generated files 37 | .idea/**/contentModel.xml 38 | 39 | # Sensitive or high-churn files 40 | .idea/**/dataSources/ 41 | .idea/**/dataSources.ids 42 | .idea/**/dataSources.local.xml 43 | .idea/**/sqlDataSources.xml 44 | .idea/**/dynamic.xml 45 | .idea/**/uiDesigner.xml 46 | .idea/**/dbnavigator.xml 47 | 48 | ### Go template 49 | # If you prefer the allow list template instead of the deny list, see community template: 50 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 51 | # 52 | # Binaries for programs and plugins 53 | *.exe 54 | *.exe~ 55 | *.dll 56 | *.so 57 | *.dylib 58 | 59 | # Test binary, built with `go v -c` 60 | *.v 61 | 62 | # Output of the go coverage tool, specifically when used with LiteIDE 63 | *.out 64 | 65 | # Dependency directories (remove the comment below to include it) 66 | # vendor/ 67 | 68 | # Go workspace key 69 | go.work 70 | 71 | ### MacOS Template 72 | 73 | .DS_Store 74 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/poly-gun/go-telemetry 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.0 6 | 7 | require ( 8 | go.opentelemetry.io/contrib/bridges/otelslog v0.8.0 9 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 10 | go.opentelemetry.io/otel v1.34.0 11 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 12 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0 13 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 14 | go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 15 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0 16 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 17 | go.opentelemetry.io/otel/exporters/zipkin v1.34.0 18 | go.opentelemetry.io/otel/log v0.10.0 19 | go.opentelemetry.io/otel/sdk v1.34.0 20 | go.opentelemetry.io/otel/sdk/log v0.10.0 21 | go.opentelemetry.io/otel/sdk/metric v1.34.0 22 | go.opentelemetry.io/otel/trace v1.34.0 23 | golang.org/x/term v0.29.0 24 | ) 25 | 26 | require ( 27 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 28 | github.com/felixge/httpsnoop v1.0.4 // indirect 29 | github.com/go-logr/logr v1.4.2 // indirect 30 | github.com/go-logr/stdr v1.2.2 // indirect 31 | github.com/google/uuid v1.6.0 // indirect 32 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect 33 | github.com/openzipkin/zipkin-go v0.4.3 // indirect 34 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 35 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect 36 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 37 | go.opentelemetry.io/proto/otlp v1.5.0 // indirect 38 | golang.org/x/net v0.35.0 // indirect 39 | golang.org/x/sys v0.30.0 // indirect 40 | golang.org/x/text v0.22.0 // indirect 41 | google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 // indirect 42 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect 43 | google.golang.org/grpc v1.70.0 // indirect 44 | google.golang.org/protobuf v1.36.5 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package telemetry_test 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "time" 7 | 8 | "github.com/poly-gun/go-telemetry" 9 | 10 | "go.opentelemetry.io/otel" 11 | "go.opentelemetry.io/otel/attribute" 12 | "go.opentelemetry.io/otel/trace" 13 | ) 14 | 15 | // ctx, cancel represent the server's runtime context and cancellation handler. 16 | var ctx, cancel = context.WithCancel(context.Background()) 17 | 18 | func Example() { 19 | defer cancel() // Eventually stop the open-telemetry client. 20 | 21 | ctx, span := otel.Tracer("example").Start(ctx, "main", trace.WithSpanKind(trace.SpanKindUnspecified)) 22 | 23 | _ = ctx // Real implementation is likely to make use of the ctx. 24 | 25 | // Typical use case of the span would be to defer span.End() after initialization; however, in the example, we need to 26 | // control when it ends in order to capture the output and write it out as the example. 27 | 28 | // defer span.End() 29 | 30 | // Add an event (in many observability tools, this gets represented as a log message). 31 | span.AddEvent("example-event-log-1", trace.WithAttributes(attribute.String("message", "Hello World"))) 32 | 33 | span.End() 34 | 35 | time.Sleep(5 * time.Second) 36 | 37 | // The output will include metrics and trace message(s) in JSON format, printed to standard-output. 38 | // Output: 39 | } 40 | 41 | func init() { 42 | // Setup the telemetry pipeline and cancellation handler. 43 | shutdown := telemetry.Setup(ctx, func(o *telemetry.Settings) { 44 | if os.Getenv("CI") == "" { // Example of running the program in a local, development environment. 45 | o.Zipkin.Enabled = false 46 | 47 | o.Tracer.Local = true 48 | o.Tracer.Options = nil 49 | o.Tracer.Writer = os.Stdout 50 | 51 | o.Metrics.Local = true 52 | o.Metrics.Options = nil 53 | o.Metrics.Writer = os.Stdout 54 | 55 | o.Logs.Local = true 56 | o.Logs.Options = nil 57 | o.Logs.Writer = os.Stdout 58 | } else { 59 | o.Zipkin.URL = "http://zipkin.istio-system.svc.cluster.local:9411" 60 | } 61 | }) 62 | 63 | // Initialize the telemetry interrupt handler. 64 | telemetry.Interrupt(ctx, cancel, shutdown) 65 | } 66 | -------------------------------------------------------------------------------- /interrupt_test.go: -------------------------------------------------------------------------------- 1 | package telemetry_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log/slog" 8 | "os" 9 | "syscall" 10 | "testing" 11 | "time" 12 | 13 | "github.com/poly-gun/go-telemetry" 14 | ) 15 | 16 | func TestInterrupt(t *testing.T) { 17 | const service = "test-service" 18 | const version = "0.0.0" 19 | 20 | t.Setenv("OTEL_RESOURCE_ATTRIBUTES", fmt.Sprintf("service.name=%s,service.version=%s", service, version)) 21 | 22 | t.Run("Telemetry-Graceful-Shutdown", func(t *testing.T) { 23 | ctx, cancel := context.WithCancel(context.Background()) 24 | 25 | logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 26 | AddSource: true, 27 | Level: slog.LevelDebug, 28 | ReplaceAttr: nil, 29 | })) 30 | 31 | slog.SetDefault(logger) 32 | 33 | // Telemetry Setup + Cancellation Handler 34 | shutdown := telemetry.Setup(ctx, func(options *telemetry.Settings) { 35 | options.Zipkin.Enabled = false // disabled during testing 36 | 37 | options.Tracer.Local = true 38 | options.Metrics.Local = true 39 | options.Logs.Local = true 40 | 41 | options.Metrics.Writer = io.Discard // prevent output from filling the test logs 42 | }) 43 | 44 | listener := telemetry.Interrupt(ctx, cancel, shutdown) 45 | 46 | time.Sleep(5 * time.Second) 47 | 48 | listener <- syscall.SIGTERM 49 | 50 | <-ctx.Done() 51 | }) 52 | } 53 | 54 | func ExampleInterrupt() { 55 | const service = "example-service" 56 | const version = "0.0.0" 57 | 58 | _ = os.Setenv("OTEL_RESOURCE_ATTRIBUTES", fmt.Sprintf("service.name=%s,service.version=%s", service, version)) 59 | 60 | ctx, cancel := context.WithCancel(context.Background()) 61 | 62 | // Telemetry Setup + Cancellation Handler 63 | shutdown := telemetry.Setup(ctx, func(options *telemetry.Settings) { 64 | options.Zipkin.Enabled = false // disabled during testing 65 | 66 | options.Tracer.Local = true 67 | options.Metrics.Local = true 68 | options.Logs.Local = true 69 | 70 | options.Metrics.Writer = io.Discard // prevent output from filling the test logs 71 | }) 72 | 73 | listener := telemetry.Interrupt(ctx, cancel, shutdown) 74 | 75 | time.Sleep(5 * time.Second) 76 | 77 | listener <- syscall.SIGTERM 78 | 79 | <-ctx.Done() 80 | 81 | fmt.Println("Telemetry Shutdown Complete") 82 | 83 | // Output: 84 | // Telemetry Shutdown Complete 85 | } 86 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | "time" 7 | 8 | "go.opentelemetry.io/otel/attribute" 9 | "go.opentelemetry.io/otel/trace" 10 | ) 11 | 12 | type Options struct { 13 | Headers http.Header 14 | Timeout time.Duration 15 | Name string 16 | Level slog.Level 17 | 18 | Attributes []attribute.KeyValue 19 | } 20 | 21 | func (o *Options) defaults() *Options { 22 | if o == nil { 23 | *o = Options{ 24 | Headers: make(http.Header), 25 | Timeout: 15 * time.Second, 26 | Name: "github.com/poly-gun/go-telemetry", 27 | Attributes: make([]attribute.KeyValue, 0), 28 | Level: slog.LevelInfo, 29 | } 30 | } 31 | 32 | if o.Headers == nil { 33 | o.Headers = make(http.Header) 34 | } 35 | 36 | if o.Timeout <= 0 { 37 | o.Timeout = 15 * time.Second 38 | } 39 | 40 | if o.Name == "" { 41 | o.Name = "github.com/poly-gun/go-telemetry" 42 | } 43 | 44 | if o.Attributes == nil { 45 | o.Attributes = make([]attribute.KeyValue, 0) 46 | } 47 | 48 | return o 49 | } 50 | 51 | type Client struct { 52 | client *http.Client 53 | 54 | options *Options 55 | } 56 | 57 | func New(settings ...func(o *Options)) *Client { 58 | options := new(Options).defaults() 59 | for _, setting := range settings { 60 | if setting != nil { 61 | setting(options) 62 | } 63 | } 64 | 65 | return &Client{ 66 | client: &http.Client{ 67 | Timeout: options.Timeout, 68 | }, 69 | options: options, 70 | } 71 | } 72 | 73 | func (c *Client) Do(r *http.Request) (*http.Response, error) { 74 | if c == nil { 75 | *c = *New() 76 | } 77 | 78 | ctx := r.Context() 79 | kind := trace.WithSpanKind(trace.SpanKindClient) 80 | links := trace.WithLinks(trace.LinkFromContext(ctx)) 81 | attributes := append(c.options.Attributes, attribute.String("url", r.URL.String()), attribute.String("method", r.Method)) 82 | ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer(c.options.Name).Start(ctx, r.URL.String(), kind, trace.WithTimestamp(time.Now()), trace.WithAttributes(attributes...), links) 83 | 84 | defer span.End() 85 | 86 | slog.Log(ctx, c.options.Level, "Log Message From HTTP Client Transport", slog.String("name", c.options.Name), slog.String("url", r.URL.String())) 87 | for key, value := range c.options.Headers { 88 | for _, v := range value { 89 | r.Header.Add(key, v) 90 | } 91 | } 92 | 93 | return c.client.Do(r) 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `go-telemetry` - OTEL HTTP Telemetry 2 | 3 | ## Documentation 4 | 5 | Official `godoc` documentation (with examples) can be found at the [Package Registry](https://pkg.go.dev/github.com/poly-gun/go-telemetry). 6 | 7 | ## Usage 8 | 9 | ###### Add Package Dependency 10 | 11 | ```bash 12 | go get -u github.com/poly-gun/go-telemetry 13 | ``` 14 | 15 | ###### Import and Implement 16 | 17 | `main.go` 18 | 19 | ```go 20 | package main 21 | 22 | import ( 23 | "bytes" 24 | "context" 25 | "encoding/json" 26 | "fmt" 27 | "os" 28 | "time" 29 | 30 | "github.com/poly-gun/go-telemetry" 31 | 32 | "go.opentelemetry.io/otel" 33 | "go.opentelemetry.io/otel/attribute" 34 | "go.opentelemetry.io/otel/trace" 35 | ) 36 | 37 | // ctx, cancel represent the server's runtime context and cancellation handler. 38 | var ctx, cancel = context.WithCancel(context.Background()) 39 | 40 | func main() { 41 | defer cancel() // Eventually stop the open-telemetry client. 42 | 43 | ctx, span := otel.Tracer("example").Start(ctx, "main", trace.WithSpanKind(trace.SpanKindUnspecified)) 44 | 45 | _ = ctx // Real implementation is likely to make use of the ctx. 46 | 47 | // Typical use case of the span would be to defer span.End() after initialization; however, in the example, we need to 48 | // control when it ends in order to capture the output and write it out as the example. 49 | 50 | // defer span.End() 51 | 52 | // Add an event (in many observability tools, this gets represented as a log message). 53 | span.AddEvent("example-event-log-1", trace.WithAttributes(attribute.String("message", "Hello World"))) 54 | 55 | span.End() 56 | 57 | time.Sleep(5 * time.Second) 58 | 59 | // Output: A metrics and trace message(s) in JSON format, printed to standard-output. 60 | } 61 | 62 | func init() { 63 | // Setup the telemetry pipeline and cancellation handler. 64 | shutdown := telemetry.Setup(ctx, func(o *telemetry.Settings) { 65 | if os.Getenv("CI") == "" { // Example of running the program in a local, development environment. 66 | o.Zipkin.Enabled = false 67 | 68 | o.Tracer.Local = true 69 | o.Tracer.Options = nil 70 | o.Tracer.Writer = os.Stdout 71 | 72 | o.Metrics.Local = true 73 | o.Metrics.Options = nil 74 | o.Metrics.Writer = os.Stdout 75 | 76 | o.Logs.Local = true 77 | o.Logs.Options = nil 78 | o.Logs.Writer = os.Stdout 79 | } else { 80 | o.Zipkin.URL = "http://zipkin.istio-system.svc.cluster.local:9411" 81 | } 82 | }) 83 | 84 | // Initialize the telemetry interrupt handler. 85 | telemetry.Interrupt(ctx, cancel, shutdown) 86 | } 87 | ``` 88 | 89 | - Please refer to the [code examples](./example_test.go) for additional usage and implementation details. 90 | - See https://pkg.go.dev/github.com/poly-gun/go-telemetry for additional documentation. 91 | 92 | ## Contributions 93 | 94 | See the [**Contributing Guide**](./CONTRIBUTING.md) for additional details on getting started. 95 | 96 | ## Task-Board 97 | 98 | - [ ] Create a Resource Detector for Kubernetes Telemetry. 99 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = true 7 | max_line_length = 120 8 | tab_width = 4 9 | trim_trailing_whitespace = true 10 | ij_continuation_indent_size = 8 11 | ij_formatter_off_tag = @formatter:off 12 | ij_formatter_on_tag = @formatter:on 13 | ij_formatter_tags_enabled = true 14 | ij_smart_tabs = false 15 | ij_visual_guides = 120, 160 16 | ij_wrap_on_typing = false 17 | 18 | [.editorconfig] 19 | ij_editorconfig_align_group_field_declarations = false 20 | ij_editorconfig_space_after_colon = false 21 | ij_editorconfig_space_after_comma = true 22 | ij_editorconfig_space_before_colon = false 23 | ij_editorconfig_space_before_comma = false 24 | ij_editorconfig_spaces_around_assignment_operators = true 25 | 26 | [{*.bash,*.sh,*.zsh}] 27 | ij_shell_binary_ops_start_line = false 28 | ij_shell_keep_column_alignment_padding = false 29 | ij_shell_minify_program = false 30 | ij_shell_redirect_followed_by_space = false 31 | ij_shell_switch_cases_indented = false 32 | ij_shell_use_unix_line_separator = true 33 | 34 | [{*.go,*.go2}] 35 | indent_style = tab 36 | max_line_length = 160 37 | ij_continuation_indent_size = 4 38 | ij_visual_guides = 120, 160 39 | ij_go_GROUP_CURRENT_PROJECT_IMPORTS = true 40 | ij_go_add_leading_space_to_comments = true 41 | ij_go_add_parentheses_for_single_import = false 42 | ij_go_call_parameters_new_line_after_left_paren = true 43 | ij_go_call_parameters_right_paren_on_new_line = true 44 | ij_go_call_parameters_wrap = off 45 | ij_go_fill_paragraph_width = 160 46 | ij_go_group_stdlib_imports = true 47 | ij_go_import_sorting = gofmt 48 | ij_go_keep_indents_on_empty_lines = false 49 | ij_go_local_group_mode = project 50 | ij_go_local_package_prefixes = 51 | ij_go_move_all_imports_in_one_declaration = true 52 | ij_go_move_all_stdlib_imports_in_one_group = true 53 | ij_go_remove_redundant_import_aliases = true 54 | ij_go_run_go_fmt_on_reformat = true 55 | ij_go_use_back_quotes_for_imports = false 56 | ij_go_wrap_comp_lit = off 57 | ij_go_wrap_comp_lit_newline_after_lbrace = true 58 | ij_go_wrap_comp_lit_newline_before_rbrace = true 59 | ij_go_wrap_func_params = off 60 | ij_go_wrap_func_params_newline_after_lparen = true 61 | ij_go_wrap_func_params_newline_before_rparen = true 62 | ij_go_wrap_func_result = off 63 | ij_go_wrap_func_result_newline_after_lparen = true 64 | ij_go_wrap_func_result_newline_before_rparen = true 65 | 66 | [{*.markdown,*.md}] 67 | ij_markdown_force_one_space_after_blockquote_symbol = true 68 | ij_markdown_force_one_space_after_header_symbol = true 69 | ij_markdown_force_one_space_after_list_bullet = true 70 | ij_markdown_force_one_space_between_words = true 71 | ij_markdown_format_tables = true 72 | ij_markdown_insert_quote_arrows_on_wrap = true 73 | ij_markdown_keep_indents_on_empty_lines = false 74 | ij_markdown_keep_line_breaks_inside_text_blocks = true 75 | ij_markdown_max_lines_around_block_elements = 1 76 | ij_markdown_max_lines_around_header = 1 77 | ij_markdown_max_lines_between_paragraphs = 1 78 | ij_markdown_min_lines_around_block_elements = 1 79 | ij_markdown_min_lines_around_header = 1 80 | ij_markdown_min_lines_between_paragraphs = 1 81 | ij_markdown_wrap_text_if_long = true 82 | ij_markdown_wrap_text_inside_blockquotes = true 83 | ij_markdown_continuation_indent_size = 4 84 | ij_markdown_indent_size = 4 85 | ij_markdown_tab_width = 4 86 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | ## Package Publication 4 | 5 | _The following section refers to publishing package(s) to https://pkg.go.dev._ 6 | 7 | - See GO's [*Publishing a Module*](https://go.dev/doc/modules/publishing) for additional details. 8 | 9 | 1. Establish a [`LICENSE`](https://spdx.org/licenses/) to the project. 10 | 2. Ensure dependencies are updated. 11 | ```bash 12 | go mod tidy 13 | ``` 14 | 3. Sync the working tree's `HEAD` with its remote. 15 | ```bash 16 | git add . 17 | git commit --message "" 18 | git push --set-upstream origin main 19 | ``` 20 | 4. Assign a tag and push. 21 | ```bash 22 | git tag "v$(head VERSION)" && git push origin "v$(head VERSION)" 23 | ``` 24 | 5. Make the module available, publicly. 25 | ```bash 26 | GOPROXY=proxy.golang.org go list -mutex "github.com/poly-gun/example@v$(head VERSION)" 27 | ``` 28 | 29 | Adding the package to `pkg.go.dev` may need to be requested. Navigate to the mirror's expected url, and follow 30 | instructions for requesting the addition. 31 | 32 | - Example: https://dev.go.dev/github.com/poly-gun/example 33 | 34 | Upon successful request, a message should be displayed: 35 | 36 | > _We're still working on “github.com/poly-gun/example”. Check back in a few minutes!_ 37 | 38 | For any other issues, consult the [official](https://pkg.go.dev/about#adding-a-package) documentation. 39 | 40 | ### Testing 41 | 42 | ###### Basic 43 | 44 | ```bash 45 | go test ./... 46 | ``` 47 | 48 | ###### Testing with Useful Logging 49 | 50 | ```bash 51 | go test -c "./path-with-tests" -o pkg.test -json 52 | 53 | go tool test2json -t ./pkg.test -test.failfast -test.fullpath -test.v -test.paniconexit0 54 | ``` 55 | 56 | *Simplified* 57 | 58 | ```bash 59 | go test ./... -json 60 | ``` 61 | 62 | ### Pre-Commit 63 | 64 | The following project makes use of `pre-commit` for local-development `git-hooks`. These hooks are useful 65 | in cases such as preventing secrets from getting pushed into version-control. 66 | 67 | See the [`.pre-commit-config.yaml`](.pre-commit-config.yaml) for implementation specifics. 68 | 69 | #### Local Setup 70 | 71 | 1. Install pre-commit from https://pre-commit.com/#install. 72 | 2. Auto-update the config to the latest repos' versions by executing `pre-commit autoupdate`. 73 | 3. Install with `pre-commit install`. 74 | 75 | #### General Command Reference(s) 76 | 77 | **Update the configuration's upstreams** 78 | 79 | ```bash 80 | pre-commit autoupdate 81 | ``` 82 | 83 | **Install `pre-commit` to local instance** 84 | 85 | ```bash 86 | pre-commit install 87 | ``` 88 | 89 | ## Documentation 90 | 91 | Tool `godoc` is required to render the documentation, which includes examples. 92 | 93 | - See [`doc.go`](./doc.go) for code-specific package documentation. 94 | 95 | Installation Steps: 96 | 97 | 1. Install `godoc`. 98 | ```bash 99 | go install golang.org/x/tools/cmd/godoc@latest 100 | ``` 101 | 1. Backup shell profile and update `PATH`. 102 | ```bash 103 | cp ~/.zshrc ~/.zshrc.bak 104 | printf "export PATH=\"\${PATH}:%s\"\n" "$(go env --json | jq -r ".GOPATH")/bin" >> ~/.zshrc 105 | source ~/.zshrc 106 | ``` 107 | 1. Start the `godoc` server. 108 | ```bash 109 | godoc -http=:6060 110 | ``` 111 | 1. Open the webpage. 112 | ```bash 113 | open "http://localhost:6060/pkg/" 114 | ``` 115 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "io" 5 | 6 | "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" 7 | "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" 8 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" 9 | "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" 10 | "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" 11 | "go.opentelemetry.io/otel/propagation" 12 | "go.opentelemetry.io/otel/sdk/metric" 13 | ) 14 | 15 | // Zipkin represents the configuration for a Zipkin collector. 16 | // URL specifies the Zipkin collector URL, defaulting to "http://opentelemetry-collector.observability.svc.cluster.local:9441". 17 | // Enabled determines if the Zipkin collector is active. Default is true. 18 | type Zipkin struct { 19 | // URL - Zipkin collector url - defaults to "http://opentelemetry-collector.observability.svc.cluster.local:9441". 20 | URL string 21 | 22 | // Enabled will enable the Zipkin collector. Default is true. 23 | Enabled bool 24 | } 25 | 26 | // Tracer represents a tracer configuration for OpenTelemetry. 27 | type Tracer struct { 28 | // Options represents [otlptracehttp.Option] configurations. 29 | // 30 | // Defaults: 31 | // 32 | // - otlptracehttp.WithInsecure() 33 | // - otlptracehttp.WithEndpoint("opentelemetry-collector.observability.svc.cluster.local:4318") 34 | Options []otlptracehttp.Option 35 | 36 | // Debugger configures an additional [stdouttrace.Exporter] if not nil. Defaults nil. 37 | Debugger *stdouttrace.Exporter 38 | 39 | // Local will prevent an external tracer from getting used as a provider. If true, forces [Tracer.Debugger] configuration. Default is false. 40 | Local bool 41 | 42 | // Writer is an optional [io.Writer] for usage when [Tracer.Local] or [Tracer.Debugger] options are configured. Defaults to [os.Stdout]. 43 | Writer io.Writer 44 | } 45 | 46 | type Metrics struct { 47 | // Options represents [otlpmetrichttp.Option] configurations. 48 | // 49 | // Defaults: 50 | // 51 | // - otlpmetrichttp.WithInsecure() 52 | // - otlpmetrichttp.WithEndpoint("opentelemetry-collector.observability.svc.cluster.local:4318") 53 | Options []otlpmetrichttp.Option 54 | 55 | // Debugger configures an additional [metric.Exporter] if not nil. Defaults nil. 56 | Debugger metric.Exporter 57 | 58 | // Local will prevent an external metrics provider from getting used. If true, forces [Metrics.Debugger] configuration. Default is false. 59 | Local bool 60 | 61 | // Writer is an optional [io.Writer] for usage when [Metrics.Local] or [Metrics.Debugger] options are configured. Defaults to [os.Stdout]. 62 | Writer io.Writer 63 | } 64 | 65 | type Logs struct { 66 | // Logs represents [otlploghttp.Option] configurations. 67 | // 68 | // Defaults: 69 | // 70 | // - otlploghttp.WithInsecure() 71 | // - otlploghttp.WithEndpoint("http://zipkin.istio-system.svc.cluster.local:9411") 72 | Options []otlploghttp.Option 73 | 74 | // Debugger configures an additional [stdoutlog.Exporter] if not nil. Defaults nil. 75 | Debugger *stdoutlog.Exporter 76 | 77 | // Local will prevent an external log exporter from getting used as a processor. If true, forces [Logs.Debugger] configuration. Default is false. 78 | Local bool 79 | 80 | // Writer is an optional [io.Writer] for usage when [Logs.Local] or [Logs.Debugger] options are configured. Defaults to [os.Stdout]. 81 | Writer io.Writer 82 | } 83 | 84 | type Settings struct { 85 | // Zipkin represents a zipkin collector. 86 | Zipkin *Zipkin 87 | 88 | // Tracer represents [otlptracehttp.Option] configurations. 89 | Tracer *Tracer 90 | 91 | // Metrics represents [otlpmetrichttp.Option] configurations. 92 | Metrics *Metrics 93 | 94 | // Logs represents [otlploghttp.Option] configurations. 95 | Logs *Logs 96 | 97 | // Propagators ... 98 | // 99 | // Defaults: 100 | // 101 | // - [propagation.TraceContext] 102 | // - [propagation.Baggage] 103 | Propagators []propagation.TextMapPropagator 104 | } 105 | 106 | type Variadic func(options *Settings) 107 | 108 | func Options() *Settings { 109 | return &Settings{ 110 | Zipkin: &Zipkin{ 111 | URL: "http://opentelemetry-collector.observability.svc.cluster.local:9441", 112 | Enabled: true, 113 | }, 114 | Metrics: &Metrics{ 115 | Options: []otlpmetrichttp.Option{ 116 | otlpmetrichttp.WithInsecure(), 117 | otlpmetrichttp.WithEndpoint("opentelemetry-collector.observability.svc.cluster.local:4318"), 118 | }, 119 | }, 120 | Tracer: &Tracer{ 121 | Options: []otlptracehttp.Option{ 122 | otlptracehttp.WithInsecure(), 123 | otlptracehttp.WithEndpoint("opentelemetry-collector.observability.svc.cluster.local:4318"), 124 | }, 125 | }, 126 | Logs: &Logs{ 127 | Options: []otlploghttp.Option{ 128 | otlploghttp.WithInsecure(), 129 | otlploghttp.WithEndpoint("opentelemetry-collector.observability.svc.cluster.local:4318"), 130 | }, 131 | }, 132 | Propagators: []propagation.TextMapPropagator{ 133 | propagation.TraceContext{}, 134 | propagation.Baggage{}, 135 | }, 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /telemetry_test.go: -------------------------------------------------------------------------------- 1 | package telemetry_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "log/slog" 9 | "net/http" 10 | "net/http/httptest" 11 | "os" 12 | "syscall" 13 | "testing" 14 | "time" 15 | 16 | "go.opentelemetry.io/contrib/bridges/otelslog" 17 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 18 | "go.opentelemetry.io/otel" 19 | "go.opentelemetry.io/otel/attribute" 20 | "go.opentelemetry.io/otel/trace" 21 | 22 | "github.com/poly-gun/go-telemetry" 23 | ) 24 | 25 | func Test(t *testing.T) { 26 | const service = "test-service" 27 | const version = "0.0.0" 28 | 29 | t.Setenv("OTEL_RESOURCE_ATTRIBUTES", fmt.Sprintf("service.name=%s,service.version=%s", service, version)) 30 | 31 | t.Run("Telemetry-Initialization-Metrics", func(t *testing.T) { 32 | ctx, cancel := context.WithCancel(context.Background()) 33 | 34 | logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 35 | AddSource: true, 36 | Level: slog.LevelWarn, 37 | ReplaceAttr: nil, 38 | })) 39 | 40 | slog.SetDefault(logger) 41 | 42 | var traces, metrics, logs bytes.Buffer 43 | 44 | // Telemetry Setup + Cancellation Handler 45 | shutdown := telemetry.Setup(ctx, func(options *telemetry.Settings) { 46 | options.Zipkin.Enabled = false // disabled during testing 47 | 48 | options.Tracer = &telemetry.Tracer{ 49 | Local: true, 50 | Writer: &traces, 51 | } 52 | 53 | options.Metrics = &telemetry.Metrics{ 54 | Local: true, 55 | Writer: &metrics, 56 | } 57 | 58 | options.Logs = &telemetry.Logs{ 59 | Local: true, 60 | Writer: &logs, 61 | } 62 | }) 63 | 64 | listener := telemetry.Interrupt(ctx, cancel, shutdown) 65 | 66 | t.Cleanup(func() { 67 | listener <- syscall.SIGTERM 68 | 69 | <-ctx.Done() 70 | }) 71 | 72 | time.Sleep(10 * time.Second) 73 | 74 | if metrics.Len() == 0 { 75 | t.Error("No Metrics Received") 76 | } else { 77 | t.Logf("Metrics:\n%s", metrics.String()) 78 | } 79 | }) 80 | 81 | t.Run("Telemetry-HTTP-Handler", func(t *testing.T) { 82 | t.Setenv("OTEL_RESOURCE_ATTRIBUTES", fmt.Sprintf("service.name=%s,service.version=%s", service, version)) 83 | 84 | ctx, cancel := context.WithCancel(context.Background()) 85 | 86 | var traces, metrics, logs bytes.Buffer 87 | 88 | // Telemetry Setup + Cancellation Handler 89 | shutdown := telemetry.Setup(ctx, func(options *telemetry.Settings) { 90 | options.Zipkin.Enabled = false // disabled during testing 91 | 92 | options.Tracer = &telemetry.Tracer{ 93 | Local: true, 94 | Writer: &traces, 95 | } 96 | 97 | options.Metrics = &telemetry.Metrics{ 98 | Local: true, 99 | Writer: &metrics, 100 | } 101 | 102 | options.Logs = &telemetry.Logs{ 103 | Local: true, 104 | Writer: &logs, 105 | } 106 | }) 107 | 108 | listener := telemetry.Interrupt(ctx, cancel, shutdown) 109 | 110 | t.Cleanup(func() { 111 | listener <- syscall.SIGTERM 112 | 113 | <-ctx.Done() 114 | }) 115 | 116 | mux := http.NewServeMux() 117 | 118 | mux.Handle("GET /", otelhttp.WithRouteTag("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 119 | const name = "test-endpoint" 120 | 121 | ctx := r.Context() 122 | 123 | kind := trace.WithSpanKind(trace.SpanKindServer) 124 | links := trace.WithLinks(trace.LinkFromContext(ctx)) 125 | attributes := []attribute.KeyValue{attribute.String("url", r.URL.String()), attribute.String("method", r.Method)} 126 | 127 | ctx, span := otel.Tracer(service).Start(ctx, name, kind, trace.WithAttributes(attributes...), links) 128 | labeler, _ := otelhttp.LabelerFromContext(ctx) 129 | 130 | defer span.End() 131 | 132 | logger := otelslog.NewLogger(name) 133 | 134 | labeler.Add(attribute.String("label", name)) 135 | 136 | logger.InfoContext(ctx, "Test Endpoint Logger Message") 137 | 138 | datum := map[string]interface{}{ 139 | "key-1": "value-1", 140 | "key-2": "value-2", 141 | "key-3": "value-3", 142 | } 143 | 144 | defer json.NewEncoder(w).Encode(datum) 145 | 146 | w.Header().Set("Content-Type", "application/json") 147 | w.WriteHeader(http.StatusOK) 148 | return 149 | }))) 150 | 151 | // Add HTTP instrumentation for the whole server. 152 | handler := otelhttp.NewHandler(mux, "/") 153 | 154 | server := httptest.NewServer(handler) 155 | defer server.Close() 156 | 157 | client := server.Client() 158 | request, e := http.NewRequest(http.MethodGet, server.URL, nil) 159 | if e != nil { 160 | t.Fatalf("Unexpected Error While Generating Request: %v", e) 161 | } 162 | 163 | response, e := client.Do(request) 164 | if e != nil { 165 | t.Fatalf("Unexpected Error While Sending Request: %v", e) 166 | } 167 | 168 | defer response.Body.Close() 169 | 170 | time.Sleep(10 * time.Second) 171 | 172 | t.Run("Traces", func(t *testing.T) { 173 | if traces.Len() == 0 { 174 | t.Error("Traces Not Reported") 175 | } else { 176 | t.Logf("Traces:\n%s", traces.String()) 177 | } 178 | }) 179 | 180 | t.Run("Metrics", func(t *testing.T) { 181 | if metrics.Len() == 0 { 182 | t.Error("Metrics Not Reported") 183 | } else { 184 | t.Logf("Metrics:\n%s", metrics.String()) 185 | } 186 | }) 187 | 188 | t.Run("Logs", func(t *testing.T) { 189 | if logs.Len() == 0 { 190 | t.Error("Logs Not Reported") 191 | } else { 192 | t.Logf("Logs:\n%s", logs.String()) 193 | } 194 | }) 195 | }) 196 | } 197 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /usr/bin/env bash 2 | 3 | # ==================================================================================== 4 | # Colors 5 | # ------------------------------------------------------------------------------------ 6 | 7 | black := $(shell printf "\033[30m") 8 | black-bold := $(shell printf "\033[30;1m") 9 | red := $(shell printf "\033[31m") 10 | red-bold := $(shell printf "\033[31;1m") 11 | green := $(shell printf "\033[32m") 12 | green-bold := $(shell printf "\033[32;1m") 13 | yellow := $(shell printf "\033[33m") 14 | yellow-bold := $(shell printf "\033[33;1m") 15 | blue := $(shell printf "\033[34m") 16 | blue-bold := $(shell printf "\033[34;1m") 17 | magenta := $(shell printf "\033[35m") 18 | magenta-bold := $(shell printf "\033[35;1m") 19 | cyan := $(shell printf "\033[36m") 20 | cyan-bold := $(shell printf "\033[36;1m") 21 | white := $(shell printf "\033[37m") 22 | white-bold := $(shell printf "\033[37;1m") 23 | reset := $(shell printf "\033[0m") 24 | 25 | # ==================================================================================== 26 | # Logger 27 | # ------------------------------------------------------------------------------------ 28 | 29 | time-long = $(date +%Y-%m-%d' '%H:%M:%S) 30 | time-short = $(date +%H:%M:%S) 31 | time = $(time-short) 32 | 33 | information = echo $(time) $(blue)[ DEBUG ]$(reset) 34 | warning = echo $(time) $(yellow)[ WARNING ]$(reset) 35 | exception = echo $(time) $(red)[ ERROR ]$(reset) 36 | complete = echo $(time) $(green)[ COMPLETE ]$(reset) 37 | fail = (echo $(time) $(red)[ FAILURE ]$(reset) && false) 38 | 39 | # ==================================================================================== 40 | # Utility Command(s) 41 | # ------------------------------------------------------------------------------------ 42 | 43 | url = $(shell git config --get remote.origin.url | sed -r 's/.*(\@|\/\/)(.*)(\:|\/)([^:\/]*)\/([^\/\.]*)\.git/https:\/\/\2\/\4\/\5/') 44 | 45 | repository = $(shell basename -s .git $(shell git config --get remote.origin.url)) 46 | organization = $(shell git remote -v | grep "(fetch)" | sed 's/.*\/\([^ ]*\)\/.*/\1/') 47 | package = $(shell printf "github.com/%s/%s" "$(organization)" "$(repository)") 48 | 49 | version = $(shell [ -f VERSION ] && head VERSION || echo "0.0.0") 50 | 51 | major = $(shell echo $(version) | sed "s/^\([0-9]*\).*/\1/") 52 | minor = $(shell echo $(version) | sed "s/[0-9]*\.\([0-9]*\).*/\1/") 53 | patch = $(shell echo $(version) | sed "s/[0-9]*\.[0-9]*\.\([0-9]*\).*/\1/") 54 | 55 | zero = $(shell printf "%s" "0") 56 | 57 | major-upgrade = $(shell expr $(major) + 1).$(zero).$(zero) 58 | minor-upgrade = $(major).$(shell expr $(minor) + 1).$(zero) 59 | patch-upgrade = $(major).$(minor).$(shell expr $(patch) + 1) 60 | 61 | dirty = $(shell git diff --quiet) 62 | dirty-contents = $(shell git diff --shortstat 2>/dev/null 2>/dev/null | tail -n1) 63 | 64 | # ==================================================================================== 65 | # Package-Specific Target(s) 66 | # ------------------------------------------------------------------------------------ 67 | 68 | all :: patch-release update 69 | 70 | tidy: 71 | @go mod tidy 72 | 73 | test: tidy 74 | @echo "$(red-bold)Executing Unit-Test(s) ...$(reset)" 75 | @go test -v --fullpath --cover --tags local ./... 76 | 77 | test-example: tidy 78 | @echo "$(red-bold)Executing Unit-Test(s) ...$(reset)" 79 | @go test -v --fullpath --cover --tags local ./example_test.go 80 | 81 | # --> patch 82 | 83 | update: 84 | @echo "$(magenta-bold)Updating GO Package Registry ...$(reset)" 85 | @GOPROXY=proxy.golang.org go list -m "$(package)@v$(version)" 86 | @curl --silent "https://proxy.golang.org/$(package)/@v/v$(version).info" | jq 2>/dev/null || curl --silent "https://proxy.golang.org/$(package)/@v/v$(version).info" 87 | 88 | bump-patch: test 89 | @if ! git diff --quiet --exit-code; then \ 90 | echo "$(red-bold)Dirty Working Tree$(reset) - Commit Changes and Try Again"; \ 91 | exit 1; \ 92 | else \ 93 | echo "$(patch-upgrade)" > VERSION; \ 94 | fi 95 | 96 | commit-patch: bump-patch 97 | @echo "$(blue-bold)Tag-Release (Patch)$(reset): \"$(yellow-bold)$(package)$(reset)\" - $(white-bold)$(version)$(reset)" 98 | @git add VERSION 99 | @git commit --message "Tag-Release (Patch): \"$(package)\" - $(version)" 100 | @git push --set-upstream origin main 101 | @git tag "v$(version)" 102 | @git push origin "v$(version)" 103 | @echo "$(green-bold)Published Tag$(reset): $(version)" 104 | 105 | patch-release: commit-patch 106 | 107 | # --> minor 108 | 109 | bump-minor: test 110 | @if ! git diff --quiet --exit-code; then \ 111 | echo "$(red-bold)Dirty Working Tree$(reset) - Commit Changes and Try Again"; \ 112 | exit 1; \ 113 | else \ 114 | echo "$(minor-upgrade)" > VERSION; \ 115 | fi 116 | 117 | commit-minor: bump-minor 118 | @echo "$(blue-bold)Tag-Release (Minor)$(reset): \"$(yellow-bold)$(package)$(reset)\" - $(white-bold)$(version)$(reset)" 119 | @git add VERSION 120 | @git commit --message "Tag-Release (Minor): \"$(package)\" - $(version)" 121 | @git push --set-upstream origin main 122 | @git tag "v$(version)" 123 | @git push origin "v$(version)" 124 | @echo "$(green-bold)Published Tag$(reset): $(version)" 125 | 126 | minor-release: commit-minor 127 | 128 | # --> major 129 | 130 | bump-major: test 131 | @if ! git diff --quiet --exit-code; then \ 132 | echo "$(red-bold)Dirty Working Tree$(reset) - Commit Changes and Try Again"; \ 133 | exit 1; \ 134 | else \ 135 | echo "$(major-upgrade)" > VERSION; \ 136 | fi 137 | 138 | commit-major: bump-major 139 | @echo "$(blue-bold)Tag-Release (Major)$(reset): \"$(yellow-bold)$(package)$(reset)\" - $(white-bold)$(version)$(reset)" 140 | @git add VERSION 141 | @git commit --message "Tag-Release (Major): \"$(package)\" - $(version)" 142 | @git push --set-upstream origin main 143 | @git tag "v$(version)" 144 | @git push origin "v$(version)" 145 | @echo "$(green-bold)Published Tag$(reset): $(version)" 146 | 147 | major-release: commit-major 148 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 2 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 6 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 7 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 8 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 9 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 10 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 11 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 12 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 13 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 14 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 15 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 16 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 17 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 18 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= 19 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= 20 | github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= 21 | github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= 22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 25 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 26 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 27 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 28 | go.opentelemetry.io/contrib/bridges/otelslog v0.8.0 h1:G3sKsNueSdxuACINFxKrQeimAIst0A5ytA2YJH+3e1c= 29 | go.opentelemetry.io/contrib/bridges/otelslog v0.8.0/go.mod h1:ptJm3wizguEPurZgarDAwOeX7O0iMR7l+QvIVenhYdE= 30 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= 31 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= 32 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 33 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 34 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 h1:q/heq5Zh8xV1+7GoMGJpTxM2Lhq5+bFxB29tshuRuw0= 35 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0/go.mod h1:leO2CSTg0Y+LyvmR7Wm4pUxE8KAmaM2GCVx7O+RATLA= 36 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0 h1:opwv08VbCZ8iecIWs+McMdHRcAXzjAeda3uG2kI/hcA= 37 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0/go.mod h1:oOP3ABpW7vFHulLpE8aYtNBodrHhMTrvfxUXGvqm7Ac= 38 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= 39 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= 40 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= 41 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= 42 | go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 h1:GKCEAZLEpEf78cUvudQdTg0aET2ObOZRB2HtXA0qPAI= 43 | go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0/go.mod h1:9/zqSWLCmHT/9Jo6fYeUDRRogOLL60ABLsHWS99lF8s= 44 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0 h1:czJDQwFrMbOr9Kk+BPo1y8WZIIFIK58SA1kykuVeiOU= 45 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0/go.mod h1:lT7bmsxOe58Tq+JIOkTQMCGXdu47oA+VJKLZHbaBKbs= 46 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 h1:jBpDk4HAUsrnVO1FsfCfCOTEc/MkInJmvfCHYLFiT80= 47 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0/go.mod h1:H9LUIM1daaeZaz91vZcfeM0fejXPmgCYE8ZhzqfJuiU= 48 | go.opentelemetry.io/otel/exporters/zipkin v1.34.0 h1:GSjCkoYqsnvUMCjxF18j2tCWH8fhGZYjH3iYgechPTI= 49 | go.opentelemetry.io/otel/exporters/zipkin v1.34.0/go.mod h1:h830hluwAqgSNnZbxL2rJhmAlE7/0SF9esoHVLU04Gc= 50 | go.opentelemetry.io/otel/log v0.10.0 h1:1CXmspaRITvFcjA4kyVszuG4HjA61fPDxMb7q3BuyF0= 51 | go.opentelemetry.io/otel/log v0.10.0/go.mod h1:PbVdm9bXKku/gL0oFfUF4wwsQsOPlpo4VEqjvxih+FM= 52 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 53 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 54 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 55 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 56 | go.opentelemetry.io/otel/sdk/log v0.10.0 h1:lR4teQGWfeDVGoute6l0Ou+RpFqQ9vaPdrNJlST0bvw= 57 | go.opentelemetry.io/otel/sdk/log v0.10.0/go.mod h1:A+V1UTWREhWAittaQEG4bYm4gAZa6xnvVu+xKrIRkzo= 58 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 59 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 60 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 61 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 62 | go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= 63 | go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= 64 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 65 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 66 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 67 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 68 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 69 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 70 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 71 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 72 | google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 h1:35ZFtrCgaAjF7AFAK0+lRSf+4AyYnWRbH7og13p7rZ4= 73 | google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:W9ynFDP/shebLB1Hl/ESTOap2jHd6pmLXPNZC7SVDbA= 74 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M= 75 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= 76 | google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= 77 | google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= 78 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 79 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 80 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 81 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 82 | -------------------------------------------------------------------------------- /telemetry.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log/slog" 9 | "os" 10 | "reflect" 11 | "time" 12 | 13 | "go.opentelemetry.io/otel" 14 | "go.opentelemetry.io/otel/attribute" 15 | "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" 16 | "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" 17 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" 18 | "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" 19 | "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" 20 | "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" 21 | "go.opentelemetry.io/otel/exporters/zipkin" 22 | "go.opentelemetry.io/otel/log/global" 23 | "go.opentelemetry.io/otel/propagation" 24 | "go.opentelemetry.io/otel/sdk/log" 25 | "go.opentelemetry.io/otel/sdk/metric" 26 | "go.opentelemetry.io/otel/sdk/resource" 27 | "go.opentelemetry.io/otel/sdk/trace" 28 | semconv "go.opentelemetry.io/otel/semconv/v1.26.0" 29 | ) 30 | 31 | func resources(ctx context.Context) *resource.Resource { 32 | namespace := os.Getenv("POD_NAMESPACE") 33 | if namespace == "" { 34 | namespace = "local" 35 | } 36 | 37 | service := os.Getenv("POD_SERVICE") 38 | if service == "" { 39 | service = "service" 40 | } 41 | 42 | service = fmt.Sprintf("%s.%s", service, namespace) 43 | 44 | version := os.Getenv("POD_VERSION") 45 | if version == "" { 46 | version = "latest" 47 | } 48 | 49 | ip := os.Getenv("POD_IP") 50 | if ip == "" { 51 | ip = "0.0.0.0" 52 | } 53 | 54 | name := os.Getenv("POD_NAME") 55 | if name == "" { 56 | name = "unknown" 57 | } 58 | 59 | options := []resource.Option{ 60 | resource.WithFromEnv(), // Discover and provide attributes from OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME environment variables. 61 | // resource.WithTelemetrySDK(), // Discover and provide information about the OpenTelemetry SDK used. 62 | // resource.WithOS(), // Discover and provide OS information. 63 | // resource.WithHost(), // Discover and provide host information. 64 | resource.WithSchemaURL(semconv.SchemaURL), 65 | resource.WithContainer(), 66 | resource.WithContainerID(), 67 | resource.WithHost(), 68 | resource.WithAttributes( 69 | semconv.ServiceName(service), 70 | semconv.ServiceNamespaceKey.String(namespace), 71 | semconv.ServiceVersionKey.String(version), 72 | 73 | attribute.String("node_id", fmt.Sprintf("sidecar~%s~%s.%s~cluster.local", ip, name, namespace)), 74 | ), 75 | } 76 | 77 | instance, e := resource.New(ctx, options...) 78 | if errors.Is(e, resource.ErrPartialResource) || errors.Is(e, resource.ErrSchemaURLConflict) { 79 | slog.WarnContext(ctx, "Non-Fatal Open-Telemetry Error", slog.String("error", e.Error())) 80 | } else if e != nil { 81 | e = fmt.Errorf("unable to generate exportable resource: %w", e) 82 | slog.ErrorContext(ctx, "Fatal Open-Telemetry Error", slog.String("error", e.Error()), slog.String("error-type", reflect.TypeOf(e).String())) 83 | panic(e) 84 | } 85 | 86 | // Merge a default tracer with the initial one, overwriting anything in default. 87 | instance, e = resource.Merge(resource.Default(), instance) 88 | if e != nil { 89 | e = fmt.Errorf("unable to merge resource: %w", e) 90 | slog.ErrorContext(ctx, "Fatal Open-Telemetry Error", slog.String("error", e.Error())) 91 | panic(e) 92 | } 93 | 94 | return instance 95 | } 96 | 97 | func propagator(settings *Settings) { 98 | provider := propagation.NewCompositeTextMapPropagator(settings.Propagators...) 99 | 100 | // Register the global propagation provider. 101 | otel.SetTextMapPropagator(provider) 102 | 103 | return 104 | } 105 | 106 | func traces(ctx context.Context, settings *Settings) *trace.TracerProvider { 107 | options := []trace.TracerProviderOption{ 108 | trace.WithResource(resources(ctx)), 109 | trace.WithSampler(trace.AlwaysSample()), 110 | } 111 | 112 | if settings.Tracer.Local && settings.Tracer.Debugger == nil { 113 | var e error 114 | 115 | var writer io.Writer = os.Stdout 116 | if settings.Tracer.Writer != nil { 117 | writer = settings.Tracer.Writer 118 | } 119 | 120 | settings.Tracer.Debugger, e = stdouttrace.New(stdouttrace.WithoutTimestamps(), stdouttrace.WithPrettyPrint(), stdouttrace.WithWriter(writer)) 121 | if e != nil { 122 | e = fmt.Errorf("unable to instantiate local tracer: %w", e) 123 | panic(e) 124 | } 125 | 126 | exporter := settings.Tracer.Debugger 127 | 128 | options = append(options, trace.WithBatcher(exporter, trace.WithBatchTimeout(time.Second*5))) 129 | } else if settings.Tracer.Debugger != nil { 130 | exporter := settings.Tracer.Debugger 131 | 132 | options = append(options, trace.WithBatcher(exporter, trace.WithBatchTimeout(time.Second*5))) 133 | } else { 134 | exporter, e := otlptracehttp.New(ctx, settings.Tracer.Options...) 135 | if e != nil { 136 | panic(e) 137 | } 138 | 139 | options = append(options, trace.WithBatcher(exporter, trace.WithBatchTimeout(time.Second*30))) 140 | 141 | if settings.Zipkin.Enabled { 142 | z, e := zipkin.New(settings.Zipkin.URL) 143 | if e != nil { 144 | panic(e) 145 | } 146 | 147 | options = append(options, trace.WithBatcher(z, trace.WithBatchTimeout(time.Second*30))) 148 | } 149 | } 150 | 151 | provider := trace.NewTracerProvider(options...) 152 | 153 | // Register the global tracer provider. 154 | otel.SetTracerProvider(provider) 155 | 156 | return provider 157 | } 158 | 159 | func metrics(ctx context.Context, settings *Settings) *metric.MeterProvider { 160 | // metricExporter, err := otlpmetrichttp.New(ctx, settings.Metrics.Options...) 161 | // if err != nil { 162 | // return nil, err 163 | // } 164 | // 165 | // meterProvider := metric.NewMeterProvider( 166 | // metric.WithReader(metric.NewPeriodicReader(metricExporter, metric.WithInterval(30*time.Second))), 167 | // ) 168 | // return meterProvider, nil 169 | 170 | options := make([]metric.Option, 0) 171 | 172 | if settings.Metrics.Local && settings.Metrics.Debugger == nil { 173 | var e error 174 | 175 | var writer io.Writer = os.Stdout 176 | if settings.Metrics.Writer != nil { 177 | writer = settings.Metrics.Writer 178 | } 179 | 180 | settings.Metrics.Debugger, e = stdoutmetric.New(stdoutmetric.WithPrettyPrint(), stdoutmetric.WithWriter(writer)) 181 | if e != nil { 182 | e = fmt.Errorf("unable to instantiate local metrics exporter: %w", e) 183 | panic(e) 184 | } 185 | 186 | exporter := settings.Metrics.Debugger 187 | 188 | options = append(options, metric.WithReader(metric.NewPeriodicReader(exporter, metric.WithInterval(5*time.Second)))) 189 | } else if settings.Metrics.Debugger != nil { 190 | exporter := settings.Metrics.Debugger 191 | 192 | options = append(options, metric.WithReader(metric.NewPeriodicReader(exporter, metric.WithInterval(5*time.Second)))) 193 | } else { 194 | exporter, e := otlpmetrichttp.New(ctx, settings.Metrics.Options...) 195 | if e != nil { 196 | e = fmt.Errorf("unable to instantiate primary metrics exporter: %w", e) 197 | panic(e) 198 | } 199 | 200 | options = append(options, metric.WithReader(metric.NewPeriodicReader(exporter, metric.WithInterval(30*time.Second)))) 201 | } 202 | 203 | provider := metric.NewMeterProvider(options...) 204 | 205 | // Set the global meter provider. 206 | otel.SetMeterProvider(provider) 207 | 208 | return provider 209 | } 210 | 211 | func logexporter(ctx context.Context, settings *Settings) *log.LoggerProvider { 212 | options := make([]log.LoggerProviderOption, 0) 213 | 214 | if settings.Logs.Local && settings.Logs.Debugger == nil { 215 | var e error 216 | 217 | var writer io.Writer = os.Stdout 218 | if settings.Logs.Writer != nil { 219 | writer = settings.Logs.Writer 220 | } 221 | 222 | settings.Logs.Debugger, e = stdoutlog.New(stdoutlog.WithPrettyPrint(), stdoutlog.WithWriter(writer)) 223 | if e != nil { 224 | e = fmt.Errorf("unable to instantiate local log exporter: %w", e) 225 | panic(e) 226 | } 227 | 228 | exporter := settings.Logs.Debugger 229 | 230 | options = append(options, log.WithProcessor(log.NewSimpleProcessor(exporter))) 231 | } else if settings.Logs.Debugger != nil { 232 | exporter := settings.Logs.Debugger 233 | 234 | options = append(options, log.WithProcessor(log.NewSimpleProcessor(exporter))) 235 | } else { 236 | exporter, e := otlploghttp.New(ctx, settings.Logs.Options...) 237 | if e != nil { 238 | e = fmt.Errorf("unable to instantiate primary log exporter: %w", e) 239 | panic(e) 240 | } 241 | 242 | options = append(options, log.WithProcessor(log.NewBatchProcessor(exporter))) 243 | } 244 | 245 | provider := log.NewLoggerProvider(options...) 246 | 247 | // Register the global logger provider. 248 | global.SetLoggerProvider(provider) 249 | 250 | return provider 251 | } 252 | 253 | // Setup bootstraps the OpenTelemetry pipeline. 254 | func Setup(ctx context.Context, options ...Variadic) (shutdown func(context.Context) error) { 255 | slog.DebugContext(ctx, "Starting the Telemetry Pipeline ...") 256 | 257 | o := Options() 258 | for _, option := range options { 259 | option(o) 260 | } 261 | 262 | var shutdowns []func(context.Context) error 263 | 264 | // shutdown calls cleanup functions registered via shutdownFuncs. 265 | // The errors from the calls are joined. 266 | // Each registered cleanup will be invoked once. 267 | shutdown = func(ctx context.Context) error { 268 | var e error 269 | for _, fn := range shutdowns { 270 | e = errors.Join(e, fn(ctx)) 271 | } 272 | 273 | shutdowns = nil 274 | return e 275 | } 276 | 277 | // Set up trace provider and add shutdown handler. 278 | shutdowns = append(shutdowns, traces(ctx, o).Shutdown) 279 | 280 | // Set up meter provider and add shutdown handler. 281 | shutdowns = append(shutdowns, metrics(ctx, o).Shutdown) 282 | 283 | // Set the global logger provider and add shutdown handler. 284 | shutdowns = append(shutdowns, logexporter(ctx, o).Shutdown) 285 | 286 | // Set up the global propagator. 287 | propagator(o) 288 | 289 | return 290 | } 291 | --------------------------------------------------------------------------------