├── .codecov.yml ├── .github ├── dependabot.yml ├── dependency-review-config.yml └── workflows │ ├── dep-review.yml │ ├── dep.yml │ ├── pr.yml │ └── x.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: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | project: 5 | default: 6 | threshold: 5% 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | groups: 8 | opentelemetry: 9 | patterns: 10 | - "go.opentelemetry.io/*" 11 | - package-ecosystem: github-actions 12 | directory: "/" 13 | schedule: 14 | interval: daily 15 | -------------------------------------------------------------------------------- /.github/dependency-review-config.yml: -------------------------------------------------------------------------------- 1 | fail_on_severity: 'low' 2 | allow_licenses: 3 | - 'MIT' 4 | - 'ISC' 5 | - 'MPL-2.0' 6 | - 'BSD-2-Clause' 7 | - 'BSD-3-Clause' 8 | - 'Apache-2.0' 9 | -------------------------------------------------------------------------------- /.github/workflows/dep-review.yml: -------------------------------------------------------------------------------- 1 | name: 'Dependency Review' 2 | on: [pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | dependency-review: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 'Checkout Repository' 12 | uses: actions/checkout@v4 13 | - name: 'Dependency Review' 14 | uses: actions/dependency-review-action@v4 15 | with: 16 | config-file: './.github/dependency-review-config.yml' 17 | -------------------------------------------------------------------------------- /.github/workflows/dep.yml: -------------------------------------------------------------------------------- 1 | name: 'Dependency Review' 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | dependency-review: 13 | runs-on: ubuntu-latest 14 | env: 15 | FIRST_COMMIT_SHA: 70ac27bccf4d61fb528c15a1e790f0e6257e9da5 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v4 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v4 21 | with: 22 | head-ref: HEAD 23 | base-ref: "${{ env.FIRST_COMMIT_SHA }}" 24 | config-file: './.github/dependency-review-config.yml' 25 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pr 2 | 3 | on: 4 | pull_request: 5 | 6 | # Common Go workflows from go faster 7 | # See https://github.com/go-faster/x 8 | jobs: 9 | test: 10 | uses: go-faster/x/.github/workflows/test.yml@main 11 | cover: 12 | uses: go-faster/x/.github/workflows/cover.yml@main 13 | lint: 14 | uses: go-faster/x/.github/workflows/lint.yml@main 15 | commit: 16 | uses: go-faster/x/.github/workflows/commit.yml@main 17 | -------------------------------------------------------------------------------- /.github/workflows/x.yml: -------------------------------------------------------------------------------- 1 | name: x 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | # Common Go workflows from go faster 8 | # See https://github.com/go-faster/x 9 | jobs: 10 | test: 11 | uses: go-faster/x/.github/workflows/test.yml@main 12 | cover: 13 | uses: go-faster/x/.github/workflows/cover.yml@main 14 | lint: 15 | uses: go-faster/x/.github/workflows/lint.yml@main 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @./go.test.sh 3 | 4 | coverage: 5 | @./go.coverage.sh 6 | 7 | test_fast: 8 | go test ./... 9 | 10 | tidy: 11 | go mod tidy 12 | 13 | .PHONY: tidy coverage test test_fast -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sdk [![Go Reference](https://img.shields.io/badge/go-pkg-00ADD8)](https://pkg.go.dev/github.com/go-faster/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) 2 | 3 | SDK for go-faster applications. 4 | Implements automatic setup of observability and daemonization based on environment variables. 5 | Also automatically sets up `GOMAXPROCS` and `GOMEMLIMIT`. 6 | 7 | ## Packages 8 | 9 | | Package | Description | 10 | |--------------|------------------------------------------------------------| 11 | | `autometer` | Automatic OpenTelemetry MeterProvider from environment | 12 | | `autotracer` | Automatic OpenTelemetry TracerProvider from environment | 13 | | `autologs` | Automatic OpenTelemetry LoggerProvider from environment | 14 | | `autopyro` | Automatic Grafana Pyroscope configuration from environment | 15 | | `profiler` | Explicit pprof routes | 16 | | `zctx` | context.Context and tracing support for zap | 17 | | `gold` | Golden files in tests | 18 | | `app` | Automatic setup observability and run daemon | 19 | | `autometric` | Reflect-based OpenTelemetry metric initializer | 20 | | `otelsync` | OpenTelemetry synchronous adapter for async metrics | 21 | 22 | ## Environment variables 23 | 24 | > [!WARNING] 25 | > The pprof listener is disabled by default and should be explicitly enabled by `PPROF_ADDR`. 26 | 27 | > [!IMPORTANT] 28 | > For configuring OpenTelemetry exporters, see [OpenTelemetry exporters][otel-exporter] documentation. 29 | 30 | [otel-exporter]: https://opentelemetry.io/docs/specs/otel/protocol/exporter/ 31 | 32 | Metrics and pprof can be served from same address if needed, set both addresses to the same value. 33 | 34 | ### Example 35 | 36 | #### Environment file 37 | ```bash 38 | OTEL_LOG_LEVEL=debug 39 | OTEL_EXPORTER_OTLP_PROTOCOL=grpc 40 | OTEL_EXPORTER_OTLP_INSECURE=true 41 | OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317 42 | OTEL_RESOURCE_ATTRIBUTES=service.name=go-faster.simon 43 | 44 | # metrics exporter 45 | OTEL_METRIC_EXPORT_INTERVAL=10000 46 | OTEL_METRIC_EXPORT_TIMEOUT=5000 47 | 48 | # pyroscope 49 | PYROSCOPE_URL=http://127.0.0.1:4040 50 | # should be same as service.name 51 | PYROSCOPE_APP_NAME=go-faster.simon 52 | PYROSCOPE_ENABLE=true 53 | 54 | # use new metrics 55 | OTEL_GO_X_DEPRECATED_RUNTIME_METRICS=false 56 | # generate instance id 57 | OTEL_GO_X_RESOURCE=true 58 | ``` 59 | 60 | #### Docker Compose 61 | ```yaml 62 | services: 63 | app: 64 | image: ghcr.io/go-faster/simon:0.6.1 65 | environment: 66 | - OTEL_LOG_LEVEL=debug 67 | - OTEL_EXPORTER_OTLP_PROTOCOL=grpc 68 | - OTEL_EXPORTER_OTLP_INSECURE=true 69 | - OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol:4317 70 | - OTEL_GO_X_DEPRECATED_RUNTIME_METRICS=false 71 | - OTEL_GO_X_RESOURCE=true 72 | - OTEL_METRIC_EXPORT_INTERVAL=1000 73 | - OTEL_METRIC_EXPORT_TIMEOUT=500 74 | ``` 75 | 76 | #### Kubernetes 77 | ```yaml 78 | --- 79 | apiVersion: apps/v1 80 | kind: Deployment 81 | metadata: 82 | name: simon-client 83 | namespace: simon 84 | spec: 85 | replicas: 1 86 | selector: 87 | matchLabels: 88 | app: simon-client 89 | template: 90 | metadata: 91 | labels: 92 | app: simon-client 93 | spec: 94 | containers: 95 | - name: ingest 96 | image: ghcr.io/go-faster/simon:0.6.1 97 | env: 98 | - name: OTEL_EXPORTER_OTLP_PROTOCOL 99 | value: "grpc" 100 | - name: OTEL_EXPORTER_OTLP_ENDPOINT 101 | value: "http://otel-collector.monitoring.svc.cluster.local:4317" 102 | - name: OTEL_LOG_LEVEL 103 | value: "debug" 104 | - name: OTEL_EXPORTER_OTLP_INSECURE 105 | value: "true" 106 | - name: OTEL_GO_X_DEPRECATED_RUNTIME_METRICS 107 | value: "false" 108 | - name: OTEL_METRIC_EXPORT_INTERVAL 109 | value: "1000" 110 | - name: OTEL_METRIC_EXPORT_TIMEOUT 111 | value: "500" 112 | ``` 113 | 114 | ### Reference 115 | 116 | | Name | Description | Example | Default | 117 | |---------------------------------------|----------------------------------|-------------------------|------------------------| 118 | | `AUTOMAXPROCS` | Use [automaxprocs][automaxprocs] | `0` | `1` | 119 | | `AUTOMAXPROCS_MIN` | Minimum `GOMAXPROCS` to use | `2` | `1` | 120 | | `OTEL_RESOURCE_ATTRIBUTES` | OTEL Resource attributes | `service.name=app` | | 121 | | `OTEL_SERVICE_NAME` | OTEL Service name | `app` | `unknown_service` | 122 | | `OTEL_EXPORTER_OTLP_PROTOCOL` | OTLP protocol to use | `http` | `grpc` | 123 | | `OTEL_PROPAGATORS` | OTEL Propagators | `none` | `tracecontext,baggage` | 124 | | `PPROF_ROUTES` | List of enabled pprof routes | `cmdline,profile` | See below | 125 | | `PPROF_ADDR` | Enable pprof and listen on addr | `0.0.0.0:9010` | N/A | 126 | | `OTEL_LOG_LEVEL` | Log level | `debug` | `info` | 127 | | `OTEL_LOGS_EXPORTER` | Logs exporter to use | `none` | `otlp` | 128 | | `METRICS_ADDR` | Prometheus addr (fallback) | `localhost:9464` | Prometheus addr | 129 | | `OTEL_METRICS_EXPORTER` | Metrics exporter to use | `prometheus` | `otlp` | 130 | | `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL` | Metrics OTLP protocol to use | `http` | `grpc` | 131 | | `OTEL_EXPORTER_PROMETHEUS_HOST` | Host of prometheus addr | `0.0.0.0` | `localhost` | 132 | | `OTEL_EXPORTER_PROMETHEUS_PORT` | Port of prometheus addr | `9090` | `9464` | 133 | | `OTEL_TRACES_EXPORTER` | Traces exporter to use | `otlp` | `otlp` | 134 | | `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` | Traces OTLP protocol to use | `http` | `grpc` | 135 | | `PYROSCOPE_ENABLE` | Enable Grafana Pyroscope | `true` | `false` | 136 | | `PYROSCOPE_APP_NAME` | Pyroscope `ApplicationName` | `app` | | 137 | | `PYROSCOPE_URL` | Pyroscope `ServerAddress` | `http://localhost:1234` | | 138 | | `PYROSCOPE_USER` | Pyroscope `BasicAuthUser` | `foo` | | 139 | | `PYROSCOPE_PASSWORD` | Pyroscope `BasicAuthPassword` | `bar` | | 140 | | `PYROSCOPE_TENANT_ID` | Pyroscope `TenantID` | `foo_bar` | | 141 | 142 | [automaxprocs]: https://github.com/uber-go/automaxprocs 143 | 144 | ### Metrics exporters 145 | 146 | | Value | Description | 147 | |--------------|-----------------------------| 148 | | `otlp` | **OTLP exporter (default)** | 149 | | `prometheus` | Prometheus exporter | 150 | | `none` | No exporter | 151 | 152 | ### Trace exporters 153 | 154 | | Value | Description | 155 | |--------|-----------------------------| 156 | | `otlp` | **OTLP exporter (default)** | 157 | | `none` | No exporter | 158 | 159 | 160 | 161 | ### Defaults 162 | 163 | By default, OpenTelemetry SDK tries `localhost:4318` OTLP endpoint, assuming collector is running on the localhost. 164 | 165 | If that is not true, following errors can be seen in the logs: 166 | 167 | ```json 168 | {"error": "failed to upload metrics: Post \"https://localhost:4318/v1/metrics\": dial tcp 127.0.0.1:4318: connect: connection refused"} 169 | ``` 170 | ```json 171 | {"error": "failed to upload traces: Post \"https://localhost:4318/v1/traces\": dial tcp 127.0.0.1:4318: connect: connection refused"} 172 | ``` 173 | 174 | To fix that, configure exporters accordingly. For example, this will disable both metrics and traces exporters: 175 | 176 | ```bash 177 | export OTEL_TRACES_EXPORTER="none" 178 | export OTEL_METRICS_EXPORTER="none" 179 | export OTEL_LOGS_EXPORTER="none" 180 | ``` 181 | 182 | To enable Prometheus exporter, set `OTEL_METRICS_EXPORTER=prometheus` and `OTEL_EXPORTER_PROMETHEUS_HOST` and `OTEL_EXPORTER_PROMETHEUS_PORT` accordingly. 183 | 184 | ```bash 185 | export OTEL_METRICS_EXPORTER="prometheus" 186 | export OTEL_EXPORTER_PROMETHEUS_HOST="0.0.0.0" 187 | export OTEL_EXPORTER_PROMETHEUS_PORT="9090" 188 | ``` 189 | 190 | ### Routes for pprof 191 | 192 | List of enabled pprof routes 193 | 194 | **Name**: `PPROF_ROUTES` 195 | 196 | **Default**: `profile,symbol,trace,goroutine,heap,threadcreate,block` 197 | 198 | ## Code coverage 199 | 200 | [![codecov](https://codecov.io/gh/go-faster/sdk/branch/main/graphs/sunburst.svg?token=cEE7AZ38Ho)](https://codecov.io/gh/go-faster/sdk) 201 | -------------------------------------------------------------------------------- /app/app.go: -------------------------------------------------------------------------------- 1 | // Package app implements OTEL, prometheus, graceful shutdown and other common application features 2 | // for go-faster projects. 3 | package app 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "log/slog" 9 | "os" 10 | "os/signal" 11 | "strconv" 12 | "time" 13 | 14 | "github.com/KimMachineGun/automemlimit/memlimit" 15 | "github.com/go-faster/errors" 16 | slogzap "github.com/samber/slog-zap/v2" 17 | "go.opentelemetry.io/otel/sdk/resource" 18 | "go.uber.org/automaxprocs/maxprocs" 19 | "go.uber.org/zap" 20 | "go.uber.org/zap/zapcore" 21 | "golang.org/x/sync/errgroup" 22 | 23 | "github.com/go-faster/sdk/autologs" 24 | "github.com/go-faster/sdk/zctx" 25 | ) 26 | 27 | const ( 28 | exitCodeOk = 0 29 | exitCodeApplicationErr = 1 30 | exitCodeWatchdog = 1 31 | ) 32 | 33 | const ( 34 | shutdownTimeout = time.Second * 5 35 | watchdogTimeout = shutdownTimeout + time.Second*5 36 | ) 37 | 38 | // Go runs f until interrupt. 39 | func Go(f func(ctx context.Context, t *Telemetry) error, op ...Option) { 40 | Run(func(ctx context.Context, _ *zap.Logger, t *Telemetry) error { 41 | return f(ctx, t) 42 | }, op...) 43 | } 44 | 45 | func defaultZapConfig() zap.Config { 46 | cfg := zap.NewProductionConfig() 47 | cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 48 | return cfg 49 | } 50 | 51 | // Run f until interrupt. 52 | // 53 | // If errors.Is(err, ctx.Err()) is valid for returned error, shutdown is considered graceful. 54 | // Context is cancelled on SIGINT. After watchdogTimeout application is forcefully terminated 55 | // with exitCodeWatchdog. 56 | func Run(f func(ctx context.Context, lg *zap.Logger, t *Telemetry) error, op ...Option) { 57 | // Apply options. 58 | opts := options{ 59 | zapConfig: defaultZapConfig(), 60 | zapTee: true, 61 | ctx: context.Background(), 62 | resourceOptions: []resource.Option{ 63 | resource.WithProcessRuntimeDescription(), 64 | resource.WithProcessRuntimeVersion(), 65 | resource.WithProcessRuntimeName(), 66 | resource.WithOS(), 67 | resource.WithFromEnv(), 68 | resource.WithTelemetrySDK(), 69 | resource.WithHost(), 70 | resource.WithProcess(), 71 | }, 72 | } 73 | opts.resourceFn = func(ctx context.Context) (*resource.Resource, error) { 74 | r, err := resource.New(ctx, opts.resourceOptions...) 75 | if err != nil { 76 | return nil, errors.Wrap(err, "new") 77 | } 78 | return resource.Merge(resource.Default(), r) 79 | } 80 | if v, err := strconv.ParseBool(os.Getenv("OTEL_ZAP_TEE")); err == nil { 81 | // Override default. 82 | opts.zapTee = v 83 | } 84 | for _, o := range op { 85 | o.apply(&opts) 86 | } 87 | 88 | ctx := opts.ctx 89 | if opts.otelZap { 90 | ctx = zctx.WithOpenTelemetryZap(ctx) 91 | } 92 | ctx, baseCtxCancel := context.WithCancel(ctx) 93 | defer baseCtxCancel() 94 | 95 | // Setup logger. 96 | if s := os.Getenv("OTEL_LOG_LEVEL"); s != "" { 97 | var lvl zapcore.Level 98 | if err := lvl.UnmarshalText([]byte(s)); err != nil { 99 | panic(err) 100 | } 101 | opts.zapConfig.Level.SetLevel(lvl) 102 | } 103 | lg, err := opts.zapConfig.Build(opts.zapOptions...) 104 | if err != nil { 105 | panic(err) 106 | } 107 | defer func() { _ = lg.Sync() }() 108 | // Add logger to root context. 109 | ctx = zctx.Base(ctx, lg) 110 | 111 | // Explicit context for graceful shutdown. 112 | shutdownCtx, cancel := signal.NotifyContext(ctx, os.Interrupt) 113 | defer cancel() 114 | 115 | lg.Info("Starting") 116 | res, err := opts.resourceFn(ctx) 117 | if err != nil { 118 | panic(fmt.Sprintf("failed to get resource: %v", err)) 119 | } 120 | 121 | m, err := newTelemetry( 122 | ctx, shutdownCtx, 123 | lg.Named("metrics"), 124 | res, 125 | opts.meterOptions, opts.tracerOptions, opts.loggerOptions, 126 | ) 127 | if err != nil { 128 | panic(err) 129 | } 130 | 131 | // Setup logs. 132 | if ctx, err = autologs.Setup(ctx, m.LoggerProvider(), opts.zapTee); err != nil { 133 | panic(fmt.Sprintf("failed to setup logs: %v", err)) 134 | } 135 | 136 | shutdownCtx = zctx.Base(shutdownCtx, zctx.From(ctx)) 137 | m.shutdownContext = shutdownCtx 138 | m.baseContext = ctx 139 | 140 | { 141 | // Automatically setting GOMAXPROCS. 142 | set := true // enabled by default 143 | if v, err := strconv.ParseBool(os.Getenv("AUTOMAXPROCS")); err == nil { 144 | set = v 145 | } 146 | minProcs := 1 147 | if v, err := strconv.Atoi(os.Getenv("AUTOMAXPROCS_MIN")); err == nil { 148 | minProcs = v 149 | } 150 | if set { 151 | if _, err := maxprocs.Set( 152 | maxprocs.Logger(lg.Sugar().Infof), 153 | maxprocs.Min(minProcs), 154 | ); err != nil { 155 | lg.Warn("Failed to set GOMAXPROCS", zap.Error(err)) 156 | } 157 | } 158 | } 159 | { 160 | // Automatically set GOMEMLIMIT. 161 | // https://github.com/KimMachineGun/automemlimit 162 | // https://tip.golang.org/doc/gc-guide#Memory_limit 163 | logger := slog.New(slogzap.Option{Level: slog.LevelDebug, Logger: lg}.NewZapHandler()) 164 | if _, err := memlimit.SetGoMemLimitWithOpts(memlimit.WithLogger(logger)); err != nil { 165 | lg.Warn("Failed to set memory limit", zap.Error(err)) 166 | } 167 | } 168 | 169 | g, ctx := errgroup.WithContext(ctx) 170 | g.Go(func() (rerr error) { 171 | defer lg.Info("Shutting down") 172 | defer func() { 173 | // Recovering panic to allow telemetry to flush. 174 | if ec := recover(); ec != nil { 175 | lg.Error("Panic", 176 | zap.String("panic", fmt.Sprintf("%v", ec)), 177 | zap.StackSkip("stack", 1), 178 | ) 179 | rerr = fmt.Errorf("shutting down (panic): %v", ec) 180 | } 181 | }() 182 | m.baseContext = ctx 183 | if err := f(m.shutdownContext, zctx.From(ctx), m); err != nil { 184 | if errors.Is(err, ctx.Err()) { 185 | // Parent context got cancelled, error is expected. 186 | // TODO(ernado): check for shutdownCtx instead. 187 | lg.Debug("Graceful shutdown") 188 | return nil 189 | } 190 | return err 191 | } 192 | 193 | // Also shutting down metrics server to stop error group. 194 | cancel() 195 | 196 | return nil 197 | }) 198 | g.Go(func() error { 199 | if err := m.run(ctx); err != nil { 200 | // Should already handle context cancellation gracefully. 201 | return errors.Wrap(err, "metrics") 202 | } 203 | return nil 204 | }) 205 | 206 | go func() { 207 | // Guaranteed way to kill application. 208 | // Helps if f is stuck, e.g. deadlock during shutdown. 209 | <-shutdownCtx.Done() 210 | lg.Info("Shutdown triggered. Waiting for graceful shutdown") 211 | time.Sleep(shutdownTimeout) 212 | baseCtxCancel() 213 | 214 | // Context is canceled, giving application time to shut down gracefully. 215 | 216 | lg.Info("Base context cancelled. Forcing shutdown") 217 | time.Sleep(watchdogTimeout) 218 | 219 | // Application is not shutting down gracefully, kill it. 220 | // This code should not be executed if f is already returned. 221 | 222 | lg.Warn("Graceful shutdown watchdog triggered: forcing hard shutdown") 223 | os.Exit(exitCodeWatchdog) 224 | }() 225 | 226 | if err := g.Wait(); err != nil { 227 | lg.Error("Failed", zap.Error(err)) 228 | os.Exit(exitCodeApplicationErr) 229 | } 230 | 231 | lg.Info("Application stopped") 232 | os.Exit(exitCodeOk) 233 | } 234 | -------------------------------------------------------------------------------- /app/option.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | 6 | "go.opentelemetry.io/otel/sdk/resource" 7 | semconv "go.opentelemetry.io/otel/semconv/v1.27.0" 8 | "go.uber.org/zap" 9 | 10 | "github.com/go-faster/sdk/autologs" 11 | "github.com/go-faster/sdk/autometer" 12 | "github.com/go-faster/sdk/autotracer" 13 | ) 14 | 15 | type options struct { 16 | zapConfig zap.Config 17 | zapOptions []zap.Option 18 | zapTee bool 19 | otelZap bool 20 | ctx context.Context 21 | 22 | meterOptions []autometer.Option 23 | tracerOptions []autotracer.Option 24 | loggerOptions []autologs.Option 25 | resourceOptions []resource.Option 26 | resourceFn func(ctx context.Context) (*resource.Resource, error) 27 | } 28 | 29 | type optionFunc func(*options) 30 | 31 | func (f optionFunc) apply(o *options) { 32 | f(o) 33 | } 34 | 35 | // Option is a functional option for the application. 36 | type Option interface { 37 | apply(o *options) 38 | } 39 | 40 | // WithZapTee sets option to tee zap logs to stderr. 41 | func WithZapTee(teeToStderr bool) Option { 42 | return optionFunc(func(o *options) { 43 | o.zapTee = teeToStderr 44 | }) 45 | } 46 | 47 | // WithZapConfig sets the default zap config for the application. 48 | func WithZapConfig(cfg zap.Config) Option { 49 | return optionFunc(func(o *options) { 50 | o.zapConfig = cfg 51 | }) 52 | } 53 | 54 | // WithZapOptions sets additional zap logger options for the application. 55 | func WithZapOptions(opts ...zap.Option) Option { 56 | return optionFunc(func(o *options) { 57 | o.zapOptions = opts 58 | }) 59 | } 60 | 61 | // WithZapOpenTelemetry enabels OpenTelemetry mode for zap. 62 | // See [zctx.WithOpenTelemetryZap]. 63 | func WithZapOpenTelemetry() Option { 64 | return optionFunc(func(o *options) { 65 | o.otelZap = true 66 | }) 67 | } 68 | 69 | // WithMeterOptions sets the default autometer options for the application. 70 | func WithMeterOptions(opts ...autometer.Option) Option { 71 | return optionFunc(func(o *options) { 72 | o.meterOptions = opts 73 | }) 74 | } 75 | 76 | // WithTracerOptions sets the default autotracer options for the application. 77 | func WithTracerOptions(opts ...autotracer.Option) Option { 78 | return optionFunc(func(o *options) { 79 | o.tracerOptions = opts 80 | }) 81 | } 82 | 83 | // WithResourceOptions sets the default resource options. 84 | // 85 | // Use before [WithResource] or [WithServiceName] to override default resource options. 86 | func WithResourceOptions(opts ...resource.Option) Option { 87 | return optionFunc(func(o *options) { 88 | o.resourceOptions = opts 89 | }) 90 | } 91 | 92 | // WithServiceName sets the default service name for the application. 93 | func WithServiceName(name string) Option { 94 | return optionFunc(func(o *options) { 95 | o.resourceOptions = append(o.resourceOptions, resource.WithAttributes(semconv.ServiceName(name))) 96 | }) 97 | } 98 | 99 | // WithServiceNamespace sets the default service namespace for the application. 100 | func WithServiceNamespace(namespace string) Option { 101 | return optionFunc(func(o *options) { 102 | o.resourceOptions = append(o.resourceOptions, resource.WithAttributes(semconv.ServiceNamespace(namespace))) 103 | }) 104 | } 105 | 106 | // WithContext sets the base context for the application. Background context is used by default. 107 | func WithContext(ctx context.Context) Option { 108 | return optionFunc(func(o *options) { 109 | o.ctx = ctx 110 | }) 111 | } 112 | 113 | // WithResource sets the function that will be called to retrieve telemetry resource for application. 114 | // 115 | // Defaults to function that enables most common resource detectors. 116 | func WithResource(fn func(ctx context.Context) (*resource.Resource, error)) Option { 117 | return optionFunc(func(o *options) { 118 | o.resourceFn = fn 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /app/profiler.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "strings" 7 | 8 | "go.uber.org/zap" 9 | 10 | "github.com/go-faster/sdk/profiler" 11 | ) 12 | 13 | func (m *Telemetry) registerProfiler(mux *http.ServeMux) { 14 | var routes []string 15 | if v := os.Getenv("PPROF_ROUTES"); v != "" { 16 | routes = strings.Split(v, ",") 17 | } 18 | if len(routes) == 1 && routes[0] == "none" { 19 | return 20 | } 21 | opt := profiler.Options{ 22 | Routes: routes, 23 | UnknownRoute: func(route string) { 24 | m.lg.Warn("Unknown pprof route", zap.String("route", route)) 25 | }, 26 | } 27 | mux.Handle("/debug/pprof/", profiler.New(opt)) 28 | } 29 | -------------------------------------------------------------------------------- /app/resource.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | "go.opentelemetry.io/otel/sdk/resource" 8 | ) 9 | 10 | // Resource returns new resource for application. 11 | // 12 | // Combines following detectors: 13 | // - ProcessRuntimeDescription 14 | // - ProcessRuntimeVersion 15 | // - ProcessRuntimeName 16 | // And merges it with default resource. 17 | // 18 | // Deprecated: use [WithResourceOptions], [WithServiceName], [WithServiceNamespace]. 19 | func Resource(ctx context.Context) (*resource.Resource, error) { 20 | opts := []resource.Option{ 21 | resource.WithProcessRuntimeDescription(), 22 | resource.WithProcessRuntimeVersion(), 23 | resource.WithProcessRuntimeName(), 24 | } 25 | r, err := resource.New(ctx, opts...) 26 | if err != nil { 27 | return nil, errors.Wrap(err, "new") 28 | } 29 | return resource.Merge(resource.Default(), r) 30 | } 31 | -------------------------------------------------------------------------------- /app/slices.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | // include clones slice and appends values to it. 4 | func include[S []E, E any](s S, v ...E) S { 5 | out := make(S, len(s)+len(v)) 6 | copy(out, s) 7 | copy(out[len(s):], v) 8 | return out 9 | } 10 | -------------------------------------------------------------------------------- /app/slices_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_include(t *testing.T) { 10 | require.Equal(t, []int{1, 2, 3}, include([]int{1, 2}, 3)) 11 | } 12 | -------------------------------------------------------------------------------- /app/telemetry.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "os" 9 | "sync" 10 | "time" 11 | 12 | "github.com/go-faster/errors" 13 | "github.com/go-logr/zapr" 14 | otelpyroscope "github.com/grafana/otel-profiling-go" 15 | promClient "github.com/prometheus/client_golang/prometheus" 16 | "github.com/prometheus/client_golang/prometheus/promhttp" 17 | "go.opentelemetry.io/contrib/instrumentation/runtime" 18 | "go.opentelemetry.io/contrib/propagators/autoprop" 19 | "go.opentelemetry.io/otel" 20 | "go.opentelemetry.io/otel/log" 21 | "go.opentelemetry.io/otel/log/noop" 22 | "go.opentelemetry.io/otel/metric" 23 | "go.opentelemetry.io/otel/propagation" 24 | "go.opentelemetry.io/otel/sdk/resource" 25 | "go.opentelemetry.io/otel/trace" 26 | "go.uber.org/zap" 27 | "golang.org/x/sync/errgroup" 28 | 29 | "github.com/go-faster/sdk/autologs" 30 | "github.com/go-faster/sdk/autometer" 31 | "github.com/go-faster/sdk/autopyro" 32 | "github.com/go-faster/sdk/autotracer" 33 | ) 34 | 35 | type httpEndpoint struct { 36 | srv *http.Server 37 | mux *http.ServeMux 38 | services []string 39 | addr string 40 | } 41 | 42 | // Deprecated: use Telemetry. 43 | type Metrics = Telemetry 44 | 45 | // Telemetry wraps all telemetry for application and helper methods for it. 46 | type Telemetry struct { 47 | lg *zap.Logger 48 | 49 | prom *promClient.Registry 50 | http []httpEndpoint 51 | 52 | tracerProvider trace.TracerProvider 53 | meterProvider metric.MeterProvider 54 | loggerProvider log.LoggerProvider 55 | shutdownContext context.Context 56 | baseContext context.Context 57 | 58 | resource *resource.Resource 59 | 60 | propagator propagation.TextMapPropagator 61 | shutdowns []shutdown 62 | } 63 | 64 | // ShutdownContext is context for triggering graceful shutdown. 65 | // It is cancelled on SIGINT. 66 | // 67 | // Base context [Telemetry.BaseContext] can be used during shutdown to finish pending operations, it will be cancelled later 68 | // on timeout. 69 | func (m *Telemetry) ShutdownContext() context.Context { 70 | return m.shutdownContext 71 | } 72 | 73 | // BaseContext is base context for the application. 74 | func (m *Telemetry) BaseContext() context.Context { 75 | return m.baseContext 76 | } 77 | 78 | func (m *Telemetry) registerShutdown(name string, fn func(ctx context.Context) error) { 79 | m.shutdowns = append(m.shutdowns, shutdown{name: name, fn: fn}) 80 | } 81 | 82 | type shutdown struct { 83 | name string 84 | fn func(ctx context.Context) error 85 | } 86 | 87 | func (m *Telemetry) String() string { 88 | return "metrics" 89 | } 90 | 91 | func (m *Telemetry) run(ctx context.Context) error { 92 | defer m.lg.Debug("Stopped metrics") 93 | wg, ctx := errgroup.WithContext(ctx) 94 | 95 | for i := range m.http { 96 | e := m.http[i] 97 | wg.Go(func() error { 98 | m.lg.Info("Starting http server", 99 | zap.Strings("services", e.services), 100 | ) 101 | if err := e.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 102 | return err 103 | } 104 | m.lg.Debug("Metrics server gracefully stopped") 105 | return nil 106 | }) 107 | } 108 | wg.Go(func() error { 109 | // Wait until g ctx canceled, then try to shut down server. 110 | baseCtx := ctx 111 | select { 112 | case <-ctx.Done(): 113 | // Non-graceful shutdown. 114 | baseCtx = context.Background() 115 | case <-m.ShutdownContext().Done(): 116 | // Graceful shutdown attempt. 117 | } 118 | 119 | m.lg.Debug("Shutting down metrics") 120 | ctx, cancel := context.WithTimeout(baseCtx, shutdownTimeout) 121 | defer cancel() 122 | 123 | // Not returning error, just reporting to log. 124 | m.shutdown(ctx) 125 | 126 | return nil 127 | }) 128 | 129 | return wg.Wait() 130 | } 131 | 132 | func (m *Telemetry) shutdown(ctx context.Context) { 133 | defer m.lg.Debug("Shut down") 134 | var wg sync.WaitGroup 135 | 136 | // Launch shutdowns in parallel. 137 | wg.Add(len(m.shutdowns)) 138 | 139 | var shutdowns []string 140 | for _, s := range m.shutdowns { 141 | var ( 142 | f = s.fn 143 | n = s.name 144 | ) 145 | shutdowns = append(shutdowns, n) 146 | go func() { 147 | defer wg.Done() 148 | if err := f(ctx); err != nil { 149 | m.lg.Error("Failed to shutdown", zap.Error(err), zap.String("name", n)) 150 | } 151 | }() 152 | } 153 | 154 | // Wait for all shutdowns to finish. 155 | m.lg.Info("Waiting for shutdowns", zap.Strings("shutdowns", shutdowns)) 156 | wg.Wait() 157 | } 158 | 159 | func (m *Telemetry) MeterProvider() metric.MeterProvider { 160 | if m.meterProvider == nil { 161 | return otel.GetMeterProvider() 162 | } 163 | return m.meterProvider 164 | } 165 | 166 | func (m *Telemetry) TracerProvider() trace.TracerProvider { 167 | if m.tracerProvider == nil { 168 | return otel.GetTracerProvider() 169 | } 170 | return m.tracerProvider 171 | } 172 | 173 | func (m *Telemetry) LoggerProvider() log.LoggerProvider { 174 | if m.loggerProvider == nil { 175 | return noop.NewLoggerProvider() 176 | } 177 | return m.loggerProvider 178 | } 179 | 180 | func (m *Telemetry) TextMapPropagator() propagation.TextMapPropagator { 181 | return m.propagator 182 | } 183 | 184 | func prometheusAddr() string { 185 | host := "localhost" 186 | port := "9464" 187 | if v := os.Getenv("OTEL_EXPORTER_PROMETHEUS_HOST"); v != "" { 188 | host = v 189 | } 190 | if v := os.Getenv("OTEL_EXPORTER_PROMETHEUS_PORT"); v != "" { 191 | port = v 192 | } 193 | return net.JoinHostPort(host, port) 194 | } 195 | 196 | type zapErrorHandler struct { 197 | lg *zap.Logger 198 | } 199 | 200 | func (z zapErrorHandler) Handle(err error) { 201 | z.lg.Error("Error", zap.Error(err)) 202 | } 203 | 204 | func newTelemetry( 205 | baseCtx, shutdownCtx context.Context, 206 | lg *zap.Logger, 207 | res *resource.Resource, 208 | meterOptions []autometer.Option, 209 | tracerOptions []autotracer.Option, 210 | logsOptions []autologs.Option, 211 | ) (*Telemetry, error) { 212 | { 213 | // Setup global OTEL logger and error handler. 214 | logger := lg.Named("otel") 215 | otel.SetLogger(zapr.NewLogger(logger)) 216 | otel.SetErrorHandler(zapErrorHandler{lg: logger}) 217 | } 218 | m := &Telemetry{ 219 | lg: lg, 220 | resource: res, 221 | 222 | shutdownContext: shutdownCtx, 223 | baseContext: baseCtx, 224 | } 225 | ctx := baseCtx 226 | { 227 | provider, stop, err := autologs.NewLoggerProvider(ctx, 228 | include(logsOptions, 229 | autologs.WithResource(res), 230 | )..., 231 | ) 232 | if err != nil { 233 | return nil, errors.Wrap(err, "logger provider") 234 | } 235 | m.loggerProvider = provider 236 | m.registerShutdown("logger", stop) 237 | } 238 | { 239 | provider, stop, err := autotracer.NewTracerProvider(ctx, 240 | include(tracerOptions, 241 | autotracer.WithResource(res), 242 | )..., 243 | ) 244 | if err != nil { 245 | return nil, errors.Wrap(err, "tracer provider") 246 | } 247 | m.tracerProvider = provider 248 | m.registerShutdown("tracer", stop) 249 | } 250 | { 251 | provider, stop, err := autometer.NewMeterProvider(ctx, 252 | include(meterOptions, 253 | autometer.WithResource(res), 254 | autometer.WithOnPrometheusRegistry(func(reg *promClient.Registry) { 255 | m.prom = reg 256 | }), 257 | )..., 258 | ) 259 | if err != nil { 260 | return nil, errors.Wrap(err, "meter provider") 261 | } 262 | m.meterProvider = provider 263 | m.registerShutdown("meter", stop) 264 | } 265 | 266 | // Automatically composited from the OTEL_PROPAGATORS environment variable. 267 | m.propagator = autoprop.NewTextMapPropagator() 268 | 269 | // Setting up go runtime metrics. 270 | if err := runtime.Start( 271 | runtime.WithMeterProvider(m.MeterProvider()), 272 | runtime.WithMinimumReadMemStatsInterval(time.Second), // export as env? 273 | ); err != nil { 274 | return nil, errors.Wrap(err, "runtime metrics") 275 | } 276 | 277 | // Setup pyroscope. 278 | if autopyro.Enabled() { 279 | stop, err := autopyro.Setup(ctx) 280 | if err != nil { 281 | return nil, errors.Wrap(err, "pyroscope") 282 | } 283 | m.registerShutdown("pyroscope", stop) 284 | // Setup pyroscope tracing integration. 285 | // See https://github.com/grafana/otel-profiling-go 286 | m.tracerProvider = otelpyroscope.NewTracerProvider(m.tracerProvider) 287 | } 288 | 289 | // Register global OTEL providers. 290 | otel.SetMeterProvider(m.MeterProvider()) 291 | otel.SetTracerProvider(m.TracerProvider()) 292 | otel.SetTextMapPropagator(m.TextMapPropagator()) 293 | 294 | // Initialize and register HTTP servers if required. 295 | // 296 | // Adding prometheus. 297 | if m.prom != nil { 298 | promAddr := prometheusAddr() 299 | if v := os.Getenv("METRICS_ADDR"); v != "" { 300 | promAddr = v 301 | } 302 | mux := http.NewServeMux() 303 | e := httpEndpoint{ 304 | srv: &http.Server{Addr: promAddr, Handler: mux}, 305 | services: []string{"prometheus"}, 306 | addr: promAddr, 307 | mux: mux, 308 | } 309 | mux.Handle("/metrics", 310 | promhttp.HandlerFor(m.prom, promhttp.HandlerOpts{}), 311 | ) 312 | m.http = append(m.http, e) 313 | } 314 | // Adding pprof. 315 | if v := os.Getenv("PPROF_ADDR"); v != "" { 316 | const serviceName = "pprof" 317 | // Search for existing endpoint. 318 | var he httpEndpoint 319 | for i, e := range m.http { 320 | if e.addr != v { 321 | continue 322 | } 323 | // Using existing endpoint 324 | he = e 325 | he.services = append(he.services, serviceName) 326 | m.http[i] = he 327 | } 328 | if he.srv == nil { 329 | // Creating new endpoint. 330 | mux := http.NewServeMux() 331 | he = httpEndpoint{ 332 | srv: &http.Server{Addr: v, Handler: mux}, 333 | addr: v, 334 | mux: mux, 335 | services: []string{serviceName}, 336 | } 337 | m.http = append(m.http, he) 338 | } 339 | m.registerProfiler(he.mux) 340 | } 341 | fields := []zap.Field{ 342 | zap.Stringer("otel.resource", res), 343 | } 344 | for _, e := range m.http { 345 | for _, s := range e.services { 346 | fields = append(fields, zap.String("http."+s, e.addr)) 347 | } 348 | name := fmt.Sprintf("http %v", e.services) 349 | m.registerShutdown(name, e.srv.Shutdown) 350 | } 351 | lg.Info("Metrics initialized", fields...) 352 | return m, nil 353 | } 354 | -------------------------------------------------------------------------------- /autologs/autologs.go: -------------------------------------------------------------------------------- 1 | package autologs 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | "strings" 8 | 9 | "github.com/go-faster/errors" 10 | "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" 11 | "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" 12 | "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" 13 | "go.opentelemetry.io/otel/log" 14 | "go.opentelemetry.io/otel/log/noop" 15 | sdklog "go.opentelemetry.io/otel/sdk/log" 16 | "go.uber.org/zap" 17 | 18 | "github.com/go-faster/sdk/zctx" 19 | ) 20 | 21 | const ( 22 | expOTLP = "otlp" 23 | expNone = "none" // no-op 24 | 25 | protoHTTP = "http" 26 | protoGRPC = "grpc" 27 | defaultProto = protoGRPC 28 | ) 29 | 30 | const ( 31 | writerStdout = "stdout" 32 | writerStderr = "stderr" 33 | ) 34 | 35 | func writerByName(name string) io.Writer { 36 | switch name { 37 | case writerStdout: 38 | return os.Stdout 39 | case writerStderr: 40 | return os.Stderr 41 | default: 42 | return io.Discard 43 | } 44 | } 45 | 46 | func getEnvOr(name, def string) string { 47 | if v := os.Getenv(name); v != "" { 48 | return v 49 | } 50 | return def 51 | } 52 | 53 | func nop(_ context.Context) error { return nil } 54 | 55 | // ShutdownFunc is a function that shuts down the MeterProvider. 56 | type ShutdownFunc func(ctx context.Context) error 57 | 58 | // NewLoggerProvider initializes new [log.LoggerProvider] with the given options from environment variables. 59 | func NewLoggerProvider(ctx context.Context, options ...Option) ( 60 | meterProvider log.LoggerProvider, 61 | meterShutdown ShutdownFunc, 62 | err error, 63 | ) { 64 | cfg := newConfig(options) 65 | lg := zctx.From(ctx) 66 | var logOptions []sdklog.LoggerProviderOption 67 | if cfg.res != nil { 68 | logOptions = append(logOptions, sdklog.WithResource(cfg.res)) 69 | } 70 | ret := func(e sdklog.Exporter) (log.LoggerProvider, func(ctx context.Context) error, error) { 71 | logOptions = append(logOptions, sdklog.WithProcessor( 72 | sdklog.NewBatchProcessor(e), 73 | )) 74 | return sdklog.NewLoggerProvider(logOptions...), e.Shutdown, nil 75 | } 76 | exporter := strings.TrimSpace(getEnvOr("OTEL_LOGS_EXPORTER", expOTLP)) 77 | switch exporter { 78 | case expOTLP: 79 | proto := os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") 80 | if proto == "" { 81 | proto = os.Getenv("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL") 82 | } 83 | if proto == "" { 84 | proto = defaultProto 85 | } 86 | lg.Debug("Using OTLP logs exporter", zap.String("protocol", proto)) 87 | switch proto { 88 | case protoHTTP: 89 | exp, err := otlploghttp.New(ctx) 90 | if err != nil { 91 | return nil, nil, errors.Wrap(err, "create OTLP HTTP logs exporter") 92 | } 93 | return ret(exp) 94 | case protoGRPC: 95 | exp, err := otlploggrpc.New(ctx) 96 | if err != nil { 97 | return nil, nil, errors.Wrap(err, "create OTLP gRPC logs exporter") 98 | } 99 | return ret(exp) 100 | default: 101 | return nil, nil, errors.Errorf("unsupported logs otlp protocol %q", proto) 102 | } 103 | case writerStdout, writerStderr: 104 | lg.Debug("Using stdout log exporter", zap.String("writer", exporter)) 105 | writer := cfg.writer 106 | if writer == nil { 107 | writer = writerByName(exporter) 108 | } 109 | exp, err := stdoutlog.New(stdoutlog.WithWriter(writer)) 110 | if err != nil { 111 | return nil, nil, errors.Wrapf(err, "create %q logs exporter", exporter) 112 | } 113 | return ret(exp) 114 | case expNone: 115 | lg.Debug("Using no-op logs exporter") 116 | return noop.NewLoggerProvider(), nop, nil 117 | default: 118 | lookup := cfg.lookup 119 | if lookup == nil { 120 | break 121 | } 122 | lg.Debug("Looking for logs exporter", zap.String("exporter", exporter)) 123 | exp, ok, err := lookup(ctx, exporter) 124 | if err != nil { 125 | return nil, nil, errors.Wrapf(err, "create %q", exporter) 126 | } 127 | if !ok { 128 | break 129 | } 130 | 131 | lg.Debug("Using user-defined log exporter", zap.String("exporter", exporter)) 132 | return ret(exp) 133 | } 134 | return nil, nil, errors.Errorf("unsupported OTEL_LOGS_EXPORTER %q", exporter) 135 | } 136 | -------------------------------------------------------------------------------- /autologs/config.go: -------------------------------------------------------------------------------- 1 | package autologs 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | sdklog "go.opentelemetry.io/otel/sdk/log" 8 | "go.opentelemetry.io/otel/sdk/resource" 9 | ) 10 | 11 | // config contains configuration options for a LoggerProvider. 12 | type config struct { 13 | res *resource.Resource 14 | writer io.Writer 15 | lookup LookupExporter 16 | } 17 | 18 | // newConfig returns a config configured with options. 19 | func newConfig(options []Option) config { 20 | conf := config{res: resource.Default()} 21 | for _, o := range options { 22 | conf = o.apply(conf) 23 | } 24 | return conf 25 | } 26 | 27 | // Option applies a configuration option value to a LoggerProvider. 28 | type Option interface { 29 | apply(config) config 30 | } 31 | 32 | // optionFunc applies a set of options to a config. 33 | type optionFunc func(config) config 34 | 35 | // apply returns a config with option(s) applied. 36 | func (o optionFunc) apply(conf config) config { 37 | return o(conf) 38 | } 39 | 40 | // WithResource associates a Resource with a LoggerProvider. This Resource 41 | // represents the entity producing telemetry and is associated with all Meters 42 | // the LoggerProvider will create. 43 | // 44 | // By default, if this Option is not used, the default Resource from the 45 | // go.opentelemetry.io/otel/sdk/resource package will be used. 46 | func WithResource(res *resource.Resource) Option { 47 | return optionFunc(func(conf config) config { 48 | conf.res = res 49 | return conf 50 | }) 51 | } 52 | 53 | // WithWriter sets writer for the stderr, stdout exporters. 54 | func WithWriter(out io.Writer) Option { 55 | return optionFunc(func(conf config) config { 56 | conf.writer = out 57 | return conf 58 | }) 59 | } 60 | 61 | // LookupExporter creates exporter by name. 62 | type LookupExporter func(ctx context.Context, name string) (sdklog.Exporter, bool, error) 63 | 64 | // WithLookupExporter sets exporter lookup function. 65 | func WithLookupExporter(lookup LookupExporter) Option { 66 | return optionFunc(func(conf config) config { 67 | conf.lookup = lookup 68 | return conf 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /autologs/config_test.go: -------------------------------------------------------------------------------- 1 | package autologs 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" 12 | "go.opentelemetry.io/otel/sdk/log" 13 | ) 14 | 15 | func TestWithLookupExporter(t *testing.T) { 16 | var lookup LookupExporter = func(ctx context.Context, name string) (log.Exporter, bool, error) { 17 | switch name { 18 | case "return_something": 19 | e, err := stdoutlog.New(stdoutlog.WithWriter(io.Discard)) 20 | return e, true, err 21 | case "return_error": 22 | return nil, false, errors.New("test error") 23 | default: 24 | return nil, false, nil 25 | } 26 | } 27 | 28 | for i, tt := range []struct { 29 | name string 30 | containsErr string 31 | }{ 32 | {"return_something", ``}, 33 | {"return_error", `test error`}, 34 | {"return_not_exist", `unsupported OTEL_LOGS_EXPORTER "return_not_exist"`}, 35 | } { 36 | tt := tt 37 | t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { 38 | t.Setenv("OTEL_LOGS_EXPORTER", tt.name) 39 | ctx := context.Background() 40 | 41 | _, _, err := NewLoggerProvider(ctx, WithLookupExporter(lookup)) 42 | if tt.containsErr != "" { 43 | require.ErrorContains(t, err, tt.containsErr) 44 | return 45 | } 46 | require.NoError(t, err) 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /autologs/setup.go: -------------------------------------------------------------------------------- 1 | package autologs 2 | 3 | import ( 4 | "context" 5 | 6 | "go.opentelemetry.io/contrib/bridges/otelzap" 7 | "go.opentelemetry.io/otel/log" 8 | "go.uber.org/zap" 9 | "go.uber.org/zap/zapcore" 10 | 11 | "github.com/go-faster/sdk/zctx" 12 | ) 13 | 14 | // Setup OpenTelemetry to zap logger bridge. 15 | func Setup(ctx context.Context, loggerProvider log.LoggerProvider, teeCore bool) (context.Context, error) { 16 | lg := zctx.From(ctx) 17 | otelCore := otelzap.NewCore("github.com/go-faster/sdk/app", 18 | otelzap.WithLoggerProvider(loggerProvider), 19 | ) 20 | wrapCore := func(core zapcore.Core) zapcore.Core { 21 | return otelCore // log only to bridge 22 | } 23 | if teeCore { 24 | wrapCore = func(core zapcore.Core) zapcore.Core { 25 | // Log both to bridge and original core. 26 | return zapcore.NewTee(core, otelCore) 27 | } 28 | } 29 | return zctx.Base(ctx, 30 | lg.WithOptions( 31 | zap.WrapCore(wrapCore), 32 | ), 33 | ), nil 34 | } 35 | -------------------------------------------------------------------------------- /autometer/autometer.go: -------------------------------------------------------------------------------- 1 | // Package autometer provides an OpenTelemetry MeterProvider creation 2 | // function. 3 | package autometer 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "io" 9 | "os" 10 | "strings" 11 | 12 | "github.com/go-faster/errors" 13 | "github.com/prometheus/client_golang/prometheus" 14 | "github.com/prometheus/client_golang/prometheus/collectors" 15 | "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" 16 | "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" 17 | otelprometheus "go.opentelemetry.io/otel/exporters/prometheus" 18 | "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" 19 | "go.opentelemetry.io/otel/metric" 20 | "go.opentelemetry.io/otel/metric/noop" 21 | sdkmetric "go.opentelemetry.io/otel/sdk/metric" 22 | "go.uber.org/zap" 23 | 24 | "github.com/go-faster/sdk/zctx" 25 | ) 26 | 27 | const ( 28 | expOTLP = "otlp" 29 | expNone = "none" // no-op 30 | expPrometheus = "prometheus" 31 | 32 | protoHTTP = "http" 33 | protoGRPC = "grpc" 34 | defaultProto = protoGRPC 35 | ) 36 | 37 | const ( 38 | writerStdout = "stdout" 39 | writerStderr = "stderr" 40 | ) 41 | 42 | func writerByName(name string) io.Writer { 43 | switch name { 44 | case writerStdout: 45 | return os.Stdout 46 | case writerStderr: 47 | return os.Stderr 48 | default: 49 | return io.Discard 50 | } 51 | } 52 | 53 | func getEnvOr(name, def string) string { 54 | if v := os.Getenv(name); v != "" { 55 | return v 56 | } 57 | return def 58 | } 59 | 60 | func noopHandler(_ context.Context) error { return nil } 61 | 62 | // ShutdownFunc is a function that shuts down the MeterProvider. 63 | type ShutdownFunc func(ctx context.Context) error 64 | 65 | // NewMeterProvider returns new metric.MeterProvider based on environment variables. 66 | func NewMeterProvider(ctx context.Context, options ...Option) ( 67 | meterProvider metric.MeterProvider, 68 | meterShutdown ShutdownFunc, 69 | err error, 70 | ) { 71 | cfg := newConfig(options) 72 | lg := zctx.From(ctx) 73 | var metricOptions []sdkmetric.Option 74 | if cfg.res != nil { 75 | metricOptions = append(metricOptions, sdkmetric.WithResource(cfg.res)) 76 | } 77 | 78 | ret := func(r sdkmetric.Reader) (metric.MeterProvider, func(ctx context.Context) error, error) { 79 | metricOptions = append(metricOptions, sdkmetric.WithReader(r)) 80 | return sdkmetric.NewMeterProvider(metricOptions...), r.Shutdown, nil 81 | } 82 | 83 | // Metrics exporter. 84 | exporter := strings.TrimSpace(getEnvOr("OTEL_METRICS_EXPORTER", expOTLP)) 85 | switch exporter { 86 | case expPrometheus: 87 | lg.Debug("Using Prometheus metrics exporter") 88 | reg := cfg.prom 89 | if reg == nil { 90 | reg = prometheus.NewPedanticRegistry() 91 | } 92 | if cfg.promCallback != nil { 93 | switch v := reg.(type) { 94 | case *prometheus.Registry: 95 | cfg.promCallback(v) 96 | } 97 | } 98 | exp, err := otelprometheus.New( 99 | otelprometheus.WithRegisterer(reg), 100 | ) 101 | if err != nil { 102 | return nil, nil, errors.Wrap(err, "create Prometheus exporter") 103 | } 104 | // Register legacy prometheus-only runtime metrics for backward compatibility. 105 | reg.MustRegister( 106 | collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), 107 | collectors.NewGoCollector(), 108 | collectors.NewBuildInfoCollector(), 109 | ) 110 | return ret(exp) 111 | case expOTLP: 112 | proto := os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") 113 | if proto == "" { 114 | proto = os.Getenv("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL") 115 | } 116 | if proto == "" { 117 | proto = defaultProto 118 | } 119 | lg.Debug("Using OTLP metrics exporter", zap.String("protocol", proto)) 120 | switch proto { 121 | case protoHTTP: 122 | exp, err := otlpmetrichttp.New(ctx) 123 | if err != nil { 124 | return nil, nil, errors.Wrap(err, "create OTLP HTTP metric exporter") 125 | } 126 | return ret(sdkmetric.NewPeriodicReader(exp)) 127 | case protoGRPC: 128 | exp, err := otlpmetricgrpc.New(ctx) 129 | if err != nil { 130 | return nil, nil, errors.Wrap(err, "create OTLP gRPC metric exporter") 131 | } 132 | return ret(sdkmetric.NewPeriodicReader(exp)) 133 | default: 134 | return nil, nil, errors.Errorf("unsupported metric OTLP protocol %q", proto) 135 | } 136 | case writerStdout, writerStderr: 137 | lg.Debug("Using stdout metrics exporter", zap.String("writer", exporter)) 138 | writer := cfg.writer 139 | if writer == nil { 140 | writer = writerByName(exporter) 141 | } 142 | enc := json.NewEncoder(writer) 143 | exp, err := stdoutmetric.New(stdoutmetric.WithEncoder(enc)) 144 | if err != nil { 145 | return nil, nil, errors.Wrapf(err, "create %q metric exporter", exporter) 146 | } 147 | return ret(sdkmetric.NewPeriodicReader(exp)) 148 | case expNone: 149 | lg.Debug("Using no-op metrics exporter") 150 | return noop.NewMeterProvider(), noopHandler, nil 151 | default: 152 | lookup := cfg.lookup 153 | if lookup == nil { 154 | break 155 | } 156 | lg.Debug("Looking for metrics exporter", zap.String("exporter", exporter)) 157 | exp, ok, err := lookup(ctx, exporter) 158 | if err != nil { 159 | return nil, nil, errors.Wrapf(err, "create %q", exporter) 160 | } 161 | if !ok { 162 | break 163 | } 164 | 165 | lg.Debug("Using user-defined metrics exporter", zap.String("exporter", exporter)) 166 | return ret(exp) 167 | } 168 | return nil, nil, errors.Errorf("unsupported OTEL_METRICS_EXPORTER %q", exporter) 169 | } 170 | -------------------------------------------------------------------------------- /autometer/autometer_test.go: -------------------------------------------------------------------------------- 1 | package autometer_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "go.opentelemetry.io/otel/sdk/resource" 10 | 11 | "github.com/go-faster/sdk/autometer" 12 | ) 13 | 14 | func TestNewMeterProvider(t *testing.T) { 15 | ctx := context.Background() 16 | res := resource.Default() 17 | t.Run("Positive", func(t *testing.T) { 18 | t.Setenv("OTEL_METRICS_EXPORTER", "none") 19 | meter, stop, err := autometer.NewMeterProvider(ctx, autometer.WithResource(res)) 20 | require.NoError(t, err) 21 | require.NotNil(t, meter) 22 | require.NotNil(t, stop) 23 | 24 | _ = meter.Meter("test") 25 | require.NoError(t, stop(ctx)) 26 | }) 27 | t.Run("Negative", func(t *testing.T) { 28 | t.Setenv("OTEL_METRICS_EXPORTER", "unsupported") 29 | meter, stop, err := autometer.NewMeterProvider(ctx, autometer.WithResource(res)) 30 | require.Error(t, err) 31 | require.Nil(t, meter) 32 | require.Nil(t, stop) 33 | }) 34 | t.Run("All", func(t *testing.T) { 35 | for _, exp := range []string{ 36 | "none", 37 | "stdout", 38 | "stderr", 39 | // "otlp", // TODO: add non-blocking dial 40 | "prometheus", 41 | } { 42 | t.Run(exp, func(t *testing.T) { 43 | t.Setenv("OTEL_METRICS_EXPORTER", exp) 44 | meter, stop, err := autometer.NewMeterProvider(ctx, autometer.WithResource(res), autometer.WithWriter(io.Discard)) 45 | require.NoError(t, err) 46 | require.NotNil(t, meter) 47 | require.NotNil(t, stop) 48 | 49 | _ = meter.Meter("test") 50 | require.NoError(t, stop(ctx)) 51 | }) 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /autometer/config.go: -------------------------------------------------------------------------------- 1 | package autometer 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | sdkmetric "go.opentelemetry.io/otel/sdk/metric" 9 | "go.opentelemetry.io/otel/sdk/resource" 10 | ) 11 | 12 | // config contains configuration options for a MeterProvider. 13 | type config struct { 14 | res *resource.Resource 15 | writer io.Writer 16 | lookup LookupExporter 17 | 18 | prom prometheus.Registerer 19 | promCallback func(reg *prometheus.Registry) 20 | } 21 | 22 | // newConfig returns a config configured with options. 23 | func newConfig(options []Option) config { 24 | conf := config{res: resource.Default()} 25 | for _, o := range options { 26 | conf = o.apply(conf) 27 | } 28 | return conf 29 | } 30 | 31 | // Option applies a configuration option value to a MeterProvider. 32 | type Option interface { 33 | apply(config) config 34 | } 35 | 36 | // optionFunc applies a set of options to a config. 37 | type optionFunc func(config) config 38 | 39 | // apply returns a config with option(s) applied. 40 | func (o optionFunc) apply(conf config) config { 41 | return o(conf) 42 | } 43 | 44 | // WithResource associates a Resource with a MeterProvider. This Resource 45 | // represents the entity producing telemetry and is associated with all Meters 46 | // the MeterProvider will create. 47 | // 48 | // By default, if this Option is not used, the default Resource from the 49 | // go.opentelemetry.io/otel/sdk/resource package will be used. 50 | func WithResource(res *resource.Resource) Option { 51 | return optionFunc(func(conf config) config { 52 | conf.res = res 53 | return conf 54 | }) 55 | } 56 | 57 | func WithPrometheusRegisterer(reg prometheus.Registerer) Option { 58 | return optionFunc(func(conf config) config { 59 | conf.prom = reg 60 | return conf 61 | }) 62 | } 63 | 64 | func WithOnPrometheusRegistry(f func(reg *prometheus.Registry)) Option { 65 | return optionFunc(func(conf config) config { 66 | conf.promCallback = f 67 | return conf 68 | }) 69 | } 70 | 71 | // WithWriter sets writer for the stderr, stdout exporters. 72 | func WithWriter(out io.Writer) Option { 73 | return optionFunc(func(conf config) config { 74 | conf.writer = out 75 | return conf 76 | }) 77 | } 78 | 79 | // LookupExporter creates exporter by name. 80 | type LookupExporter func(ctx context.Context, name string) (sdkmetric.Reader, bool, error) 81 | 82 | // WithLookupExporter sets exporter lookup function. 83 | func WithLookupExporter(lookup LookupExporter) Option { 84 | return optionFunc(func(conf config) config { 85 | conf.lookup = lookup 86 | return conf 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /autometer/config_test.go: -------------------------------------------------------------------------------- 1 | package autometer 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | sdkmetric "go.opentelemetry.io/otel/sdk/metric" 11 | ) 12 | 13 | func TestWithLookupExporter(t *testing.T) { 14 | var lookup LookupExporter = func(ctx context.Context, name string) (sdkmetric.Reader, bool, error) { 15 | switch name { 16 | case "return_something": 17 | r := sdkmetric.NewManualReader() 18 | return r, true, nil 19 | case "return_error": 20 | return nil, false, errors.New("test error") 21 | default: 22 | return nil, false, nil 23 | } 24 | } 25 | 26 | for i, tt := range []struct { 27 | name string 28 | containsErr string 29 | }{ 30 | {"return_something", ``}, 31 | {"return_error", `test error`}, 32 | {"return_not_exist", `unsupported OTEL_METRICS_EXPORTER "return_not_exist"`}, 33 | } { 34 | tt := tt 35 | t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { 36 | t.Setenv("OTEL_METRICS_EXPORTER", tt.name) 37 | ctx := context.Background() 38 | 39 | _, _, err := NewMeterProvider(ctx, WithLookupExporter(lookup)) 40 | if tt.containsErr != "" { 41 | require.ErrorContains(t, err, tt.containsErr) 42 | return 43 | } 44 | require.NoError(t, err) 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /autometric/autometric.go: -------------------------------------------------------------------------------- 1 | // Package autometric contains a simple reflect-based OpenTelemetry metric initializer. 2 | package autometric 3 | 4 | import ( 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/go-faster/errors" 10 | "go.opentelemetry.io/otel/metric" 11 | ) 12 | 13 | var ( 14 | int64CounterType = reflect.TypeOf(new(metric.Int64Counter)).Elem() 15 | int64UpDownCounterType = reflect.TypeOf(new(metric.Int64UpDownCounter)).Elem() 16 | int64HistogramType = reflect.TypeOf(new(metric.Int64Histogram)).Elem() 17 | int64GaugeType = reflect.TypeOf(new(metric.Int64Gauge)).Elem() 18 | int64ObservableCounterType = reflect.TypeOf(new(metric.Int64ObservableCounter)).Elem() 19 | int64ObservableUpDownCounterType = reflect.TypeOf(new(metric.Int64ObservableUpDownCounter)).Elem() 20 | int64ObservableGaugeType = reflect.TypeOf(new(metric.Int64ObservableGauge)).Elem() 21 | ) 22 | 23 | var ( 24 | float64CounterType = reflect.TypeOf(new(metric.Float64Counter)).Elem() 25 | float64UpDownCounterType = reflect.TypeOf(new(metric.Float64UpDownCounter)).Elem() 26 | float64HistogramType = reflect.TypeOf(new(metric.Float64Histogram)).Elem() 27 | float64GaugeType = reflect.TypeOf(new(metric.Float64Gauge)).Elem() 28 | float64ObservableCounterType = reflect.TypeOf(new(metric.Float64ObservableCounter)).Elem() 29 | float64ObservableUpDownCounterType = reflect.TypeOf(new(metric.Float64ObservableUpDownCounter)).Elem() 30 | float64ObservableGaugeType = reflect.TypeOf(new(metric.Float64ObservableGauge)).Elem() 31 | ) 32 | 33 | // InitOptions defines options for [Init]. 34 | type InitOptions struct { 35 | // Prefix defines common prefix for all metrics. 36 | Prefix string 37 | // FieldName returns name for given field. 38 | FieldName func(prefix string, sf reflect.StructField) string 39 | } 40 | 41 | func (opts *InitOptions) setDefaults() { 42 | if opts.FieldName == nil { 43 | opts.FieldName = fieldName 44 | } 45 | } 46 | 47 | func fieldName(prefix string, sf reflect.StructField) string { 48 | name := snakeCase(sf.Name) 49 | if tag, ok := sf.Tag.Lookup("name"); ok { 50 | name = tag 51 | } 52 | return prefix + name 53 | } 54 | 55 | // Init initialize metrics in given struct s using given meter. 56 | func Init(m metric.Meter, s any, opts InitOptions) error { 57 | opts.setDefaults() 58 | 59 | ptr := reflect.ValueOf(s) 60 | if !isValidPtrStruct(ptr) { 61 | return errors.Errorf("a pointer-to-struct expected, got %T", s) 62 | } 63 | 64 | var ( 65 | struct_ = ptr.Elem() 66 | structType = struct_.Type() 67 | ) 68 | for i := 0; i < struct_.NumField(); i++ { 69 | fieldType := structType.Field(i) 70 | if fieldType.Anonymous || !fieldType.IsExported() { 71 | continue 72 | } 73 | if n, ok := fieldType.Tag.Lookup("autometric"); ok && n == "-" { 74 | continue 75 | } 76 | 77 | field := struct_.Field(i) 78 | if !field.CanSet() { 79 | continue 80 | } 81 | 82 | mt, err := makeField(m, fieldType, opts) 83 | if err != nil { 84 | return errors.Wrapf(err, "field (%s).%s", structType, fieldType.Name) 85 | } 86 | field.Set(reflect.ValueOf(mt)) 87 | } 88 | 89 | return nil 90 | } 91 | 92 | func makeField(m metric.Meter, sf reflect.StructField, opts InitOptions) (any, error) { 93 | var ( 94 | name = opts.FieldName(opts.Prefix, sf) 95 | unit = sf.Tag.Get("unit") 96 | desc = sf.Tag.Get("description") 97 | boundaries []float64 98 | ) 99 | if b, ok := sf.Tag.Lookup("boundaries"); ok { 100 | switch ftyp := sf.Type; ftyp { 101 | case int64HistogramType, float64HistogramType: 102 | default: 103 | return nil, errors.Errorf("boundaries tag should be used only on histogram metrics: got %v", ftyp) 104 | } 105 | for _, val := range strings.Split(b, ",") { 106 | f, err := strconv.ParseFloat(val, 64) 107 | if err != nil { 108 | return nil, errors.Wrap(err, "parse boundaries") 109 | } 110 | boundaries = append(boundaries, f) 111 | } 112 | } 113 | 114 | switch ftyp := sf.Type; ftyp { 115 | case int64CounterType: 116 | return m.Int64Counter(name, 117 | metric.WithUnit(unit), 118 | metric.WithDescription(desc), 119 | ) 120 | case int64UpDownCounterType: 121 | return m.Int64UpDownCounter(name, 122 | metric.WithUnit(unit), 123 | metric.WithDescription(desc), 124 | ) 125 | case int64HistogramType: 126 | return m.Int64Histogram(name, 127 | metric.WithUnit(unit), 128 | metric.WithDescription(desc), 129 | metric.WithExplicitBucketBoundaries(boundaries...), 130 | ) 131 | case int64GaugeType: 132 | return m.Int64Gauge(name, 133 | metric.WithUnit(unit), 134 | metric.WithDescription(desc), 135 | ) 136 | case int64ObservableCounterType, 137 | int64ObservableUpDownCounterType, 138 | int64ObservableGaugeType: 139 | return nil, errors.New("observables are not supported") 140 | 141 | case float64CounterType: 142 | return m.Float64Counter(name, 143 | metric.WithUnit(unit), 144 | metric.WithDescription(desc), 145 | ) 146 | case float64UpDownCounterType: 147 | return m.Float64UpDownCounter(name, 148 | metric.WithUnit(unit), 149 | metric.WithDescription(desc), 150 | ) 151 | case float64HistogramType: 152 | return m.Float64Histogram(name, 153 | metric.WithUnit(unit), 154 | metric.WithDescription(desc), 155 | metric.WithExplicitBucketBoundaries(boundaries...), 156 | ) 157 | case float64GaugeType: 158 | return m.Float64Gauge(name, 159 | metric.WithUnit(unit), 160 | metric.WithDescription(desc), 161 | ) 162 | case float64ObservableCounterType, 163 | float64ObservableUpDownCounterType, 164 | float64ObservableGaugeType: 165 | return nil, errors.New("observables are not supported") 166 | default: 167 | return nil, errors.Errorf("unexpected type %v", ftyp) 168 | } 169 | } 170 | 171 | func isValidPtrStruct(ptr reflect.Value) bool { 172 | return ptr.Kind() == reflect.Pointer && 173 | ptr.Elem().Kind() == reflect.Struct 174 | } 175 | -------------------------------------------------------------------------------- /autometric/autometric_test.go: -------------------------------------------------------------------------------- 1 | // Package autometric contains a simple reflect-based OpenTelemetry metric initializer. 2 | package autometric 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | "go.opentelemetry.io/otel/metric" 11 | sdkmetric "go.opentelemetry.io/otel/sdk/metric" 12 | "go.opentelemetry.io/otel/sdk/metric/metricdata" 13 | ) 14 | 15 | func TestInit(t *testing.T) { 16 | ctx := context.Background() 17 | 18 | reader := sdkmetric.NewManualReader() 19 | mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) 20 | meter := mp.Meter("test-meter") 21 | 22 | var test struct { 23 | // Ignored fields. 24 | _ int 25 | _ metric.Int64Counter 26 | // Embedded fields. 27 | fmt.Stringer 28 | // Private fields. 29 | private int 30 | privateCounter metric.Int64Counter 31 | // Skip. 32 | SkipMe metric.Int64Counter `autometric:"-"` 33 | SkipMe2 metric.Int64ObservableCounter `autometric:"-"` 34 | 35 | Int64Counter metric.Int64Counter 36 | Int64UpDownCounter metric.Int64UpDownCounter 37 | Int64Histogram metric.Int64Histogram 38 | Int64Gauge metric.Int64Gauge 39 | Float64Counter metric.Float64Counter 40 | Float64UpDownCounter metric.Float64UpDownCounter 41 | Float64Histogram metric.Float64Histogram 42 | Float64Gauge metric.Float64Gauge 43 | 44 | Renamed metric.Int64Counter `name:"mega_counter"` 45 | WithDesc metric.Int64Counter `name:"with_desc" description:"foo"` 46 | WithUnit metric.Int64Counter `name:"with_unit" unit:"By"` 47 | WithBounds metric.Float64Histogram `name:"with_bounds" boundaries:"1,2,5"` 48 | } 49 | const prefix = "testmetrics.points." 50 | require.NoError(t, Init(meter, &test, InitOptions{ 51 | Prefix: prefix, 52 | })) 53 | 54 | require.Nil(t, test.privateCounter) 55 | 56 | require.NotNil(t, test.Int64Counter) 57 | test.Int64Counter.Add(ctx, 1) 58 | require.NotNil(t, test.Int64UpDownCounter) 59 | test.Int64UpDownCounter.Add(ctx, 1) 60 | require.NotNil(t, test.Int64Histogram) 61 | test.Int64Histogram.Record(ctx, 1) 62 | require.NotNil(t, test.Int64Gauge) 63 | test.Int64Gauge.Record(ctx, 1) 64 | require.NotNil(t, test.Float64Counter) 65 | test.Float64Counter.Add(ctx, 1) 66 | require.NotNil(t, test.Float64UpDownCounter) 67 | test.Float64UpDownCounter.Add(ctx, 1) 68 | require.NotNil(t, test.Float64Histogram) 69 | test.Float64Histogram.Record(ctx, 1) 70 | require.NotNil(t, test.Float64Gauge) 71 | test.Float64Gauge.Record(ctx, 1) 72 | 73 | require.NotNil(t, test.Renamed) 74 | test.Renamed.Add(ctx, 1) 75 | require.NotNil(t, test.WithDesc) 76 | test.WithDesc.Add(ctx, 1) 77 | require.NotNil(t, test.WithUnit) 78 | test.WithUnit.Add(ctx, 1) 79 | require.NotNil(t, test.WithBounds) 80 | test.WithBounds.Record(ctx, 1) 81 | 82 | require.NoError(t, mp.ForceFlush(ctx)) 83 | var data metricdata.ResourceMetrics 84 | require.NoError(t, reader.Collect(ctx, &data)) 85 | 86 | type MetricInfo struct { 87 | Name string 88 | Description string 89 | Unit string 90 | } 91 | var infos []MetricInfo 92 | for _, scope := range data.ScopeMetrics { 93 | for _, metric := range scope.Metrics { 94 | infos = append(infos, MetricInfo{ 95 | Name: metric.Name, 96 | Description: metric.Description, 97 | Unit: metric.Unit, 98 | }) 99 | } 100 | } 101 | require.Equal(t, 102 | []MetricInfo{ 103 | {Name: prefix + "int64_counter"}, 104 | {Name: prefix + "int64_up_down_counter"}, 105 | {Name: prefix + "int64_histogram"}, 106 | {Name: prefix + "int64_gauge"}, 107 | {Name: prefix + "float64_counter"}, 108 | {Name: prefix + "float64_up_down_counter"}, 109 | {Name: prefix + "float64_histogram"}, 110 | {Name: prefix + "float64_gauge"}, 111 | 112 | {Name: prefix + "mega_counter"}, 113 | {Name: prefix + "with_desc", Description: "foo"}, 114 | {Name: prefix + "with_unit", Unit: "By"}, 115 | {Name: prefix + "with_bounds"}, 116 | }, 117 | infos, 118 | ) 119 | } 120 | 121 | func TestInitErrors(t *testing.T) { 122 | type ( 123 | JustStruct struct{} 124 | 125 | UnexpectedType struct { 126 | Foo metric.Observable 127 | } 128 | UnsupportedInt64Observable struct { 129 | Observable metric.Int64ObservableCounter 130 | } 131 | UnsupportedFloat64Observable struct { 132 | Observable metric.Float64ObservableCounter 133 | } 134 | 135 | BoundariesOnNonHistogram struct { 136 | C metric.Int64Counter `boundaries:"foo"` 137 | } 138 | 139 | BadBoundaries struct { 140 | H metric.Float64Histogram `boundaries:"foo"` 141 | } 142 | BadBoundaries2 struct { 143 | H metric.Float64Histogram `boundaries:"foo,"` 144 | } 145 | ) 146 | 147 | for i, tt := range []struct { 148 | s any 149 | err string 150 | }{ 151 | {0, "a pointer-to-struct expected, got int"}, 152 | {JustStruct{}, "a pointer-to-struct expected, got autometric.JustStruct"}, 153 | 154 | {&UnexpectedType{}, "field (autometric.UnexpectedType).Foo: unexpected type metric.Observable"}, 155 | {&UnsupportedInt64Observable{}, "field (autometric.UnsupportedInt64Observable).Observable: observables are not supported"}, 156 | {&UnsupportedFloat64Observable{}, "field (autometric.UnsupportedFloat64Observable).Observable: observables are not supported"}, 157 | 158 | {&BoundariesOnNonHistogram{}, `field (autometric.BoundariesOnNonHistogram).C: boundaries tag should be used only on histogram metrics: got metric.Int64Counter`}, 159 | {&BadBoundaries{}, `field (autometric.BadBoundaries).H: parse boundaries: strconv.ParseFloat: parsing "foo": invalid syntax`}, 160 | {&BadBoundaries2{}, `field (autometric.BadBoundaries2).H: parse boundaries: strconv.ParseFloat: parsing "foo": invalid syntax`}, 161 | } { 162 | t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { 163 | mp := sdkmetric.NewMeterProvider() 164 | meter := mp.Meter("test-meter") 165 | require.EqualError(t, Init(meter, tt.s, InitOptions{}), tt.err) 166 | }) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /autometric/strcase.go: -------------------------------------------------------------------------------- 1 | package autometric 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | ) 7 | 8 | func snakeCase(s string) string { 9 | const delim = '_' 10 | s = strings.TrimSpace(s) 11 | for _, c := range s { 12 | if isUpper(c) { 13 | goto slow 14 | } 15 | } 16 | return s 17 | 18 | slow: 19 | var sb strings.Builder 20 | sb.Grow(len(s) + 8) 21 | 22 | var prev, curr rune 23 | for i, next := range s { 24 | switch { 25 | case isDelim(curr): 26 | if !isDelim(prev) { 27 | sb.WriteByte(delim) 28 | } 29 | case isUpper(curr): 30 | if isLower(prev) || 31 | (isUpper(prev) && isLower(next)) || 32 | (isDigit(prev) && isAlpha(next)) { 33 | sb.WriteByte(delim) 34 | } 35 | sb.WriteRune(unicode.ToLower(curr)) 36 | case i != 0: 37 | sb.WriteRune(unicode.ToLower(curr)) 38 | } 39 | prev = curr 40 | curr = next 41 | } 42 | 43 | if s != "" { 44 | if isUpper(curr) && isLower(prev) { 45 | sb.WriteByte(delim) 46 | } 47 | sb.WriteRune(unicode.ToLower(curr)) 48 | } 49 | 50 | return sb.String() 51 | } 52 | 53 | func isDelim(ch rune) bool { 54 | return unicode.IsSpace(ch) || ch == '_' || ch == '-' 55 | } 56 | 57 | func isAlpha(ch rune) bool { 58 | return isUpper(ch) || isLower(ch) 59 | } 60 | 61 | func isDigit(ch rune) bool { 62 | return ch >= '0' && ch <= '9' 63 | } 64 | 65 | func isUpper(ch rune) bool { 66 | return ch >= 'A' && ch <= 'Z' 67 | } 68 | 69 | func isLower(ch rune) bool { 70 | return ch >= 'a' && ch <= 'z' 71 | } 72 | -------------------------------------------------------------------------------- /autometric/strcase_test.go: -------------------------------------------------------------------------------- 1 | package autometric 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_snakeCase(t *testing.T) { 11 | tests := []struct { 12 | s string 13 | want string 14 | }{ 15 | {"", ""}, 16 | {"f", "f"}, 17 | {"F", "f"}, 18 | {"Foo", "foo"}, 19 | {"FooB", "foo_b"}, 20 | {" FooBar\t", "foo_bar"}, 21 | {"foo__Bar", "foo_bar"}, 22 | {"foo--Bar", "foo_bar"}, 23 | {"foo Bar", "foo_bar"}, 24 | {"foo\tBar", "foo_bar"}, 25 | {"Int64UpDownCounter", "int64_up_down_counter"}, 26 | } 27 | for i, tt := range tests { 28 | tt := tt 29 | t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { 30 | require.Equal(t, tt.want, snakeCase(tt.s)) 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /autopyro/autopyro.go: -------------------------------------------------------------------------------- 1 | package autopyro 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "runtime" 7 | "strconv" 8 | 9 | "github.com/go-faster/errors" 10 | "github.com/grafana/pyroscope-go" 11 | 12 | "github.com/go-faster/sdk/zctx" 13 | ) 14 | 15 | func noop(_ context.Context) error { return nil } 16 | 17 | // ShutdownFunc is a function that shuts down profiler. 18 | type ShutdownFunc func(ctx context.Context) error 19 | 20 | // Enabled returns true if pyroscope profiler is enabled. 21 | func Enabled() bool { 22 | s := os.Getenv("PYROSCOPE_ENABLE") 23 | v, _ := strconv.ParseBool(s) 24 | return v 25 | } 26 | 27 | // Setup pyroscope profiler. 28 | func Setup(ctx context.Context) (ShutdownFunc, error) { 29 | if !Enabled() { 30 | return noop, nil 31 | } 32 | 33 | // https://grafana.com/docs/pyroscope/latest/configure-client/language-sdks/go_push/#configure-the-go-client 34 | // These 2 lines are only required if you're using mutex or block profiling 35 | // Read the explanation below for how to set these rates: 36 | runtime.SetMutexProfileFraction(5) 37 | runtime.SetBlockProfileRate(5) 38 | 39 | lg := zctx.From(ctx).Named("pyroscope") 40 | if os.Getenv("PPROF_ADDR") != "" { 41 | lg.Warn("pprof server is enabled, but can conflict with pyroscope (i.e. not being able to get profiles from pprof endpoints)") 42 | } 43 | 44 | profiler, err := pyroscope.Start(pyroscope.Config{ 45 | ApplicationName: os.Getenv("PYROSCOPE_APP_NAME"), 46 | ServerAddress: os.Getenv("PYROSCOPE_URL"), 47 | BasicAuthUser: os.Getenv("PYROSCOPE_USER"), 48 | BasicAuthPassword: os.Getenv("PYROSCOPE_PASSWORD"), 49 | TenantID: os.Getenv("PYROSCOPE_TENANT_ID"), 50 | 51 | Logger: lg.Sugar(), 52 | 53 | // TODO: also configure from environment if needed, like PPROF_ROUTES 54 | ProfileTypes: []pyroscope.ProfileType{ 55 | // these profile types are enabled by default: 56 | pyroscope.ProfileCPU, 57 | pyroscope.ProfileAllocObjects, 58 | pyroscope.ProfileAllocSpace, 59 | pyroscope.ProfileInuseObjects, 60 | pyroscope.ProfileInuseSpace, 61 | 62 | // these profile types are optional: 63 | pyroscope.ProfileGoroutines, 64 | pyroscope.ProfileMutexCount, 65 | pyroscope.ProfileMutexDuration, 66 | pyroscope.ProfileBlockCount, 67 | pyroscope.ProfileBlockDuration, 68 | }, 69 | }) 70 | if err != nil { 71 | return noop, errors.Wrap(err, "start") 72 | } 73 | 74 | return func(ctx context.Context) error { 75 | return profiler.Stop() 76 | }, nil 77 | } 78 | -------------------------------------------------------------------------------- /autotracer/autotracer.go: -------------------------------------------------------------------------------- 1 | // Package autotracer provides an OpenTelemetry TracerProvider creation 2 | // function. 3 | package autotracer 4 | 5 | import ( 6 | "context" 7 | "io" 8 | "os" 9 | "strings" 10 | 11 | "github.com/go-faster/errors" 12 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 13 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" 14 | "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" 15 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 16 | "go.opentelemetry.io/otel/trace" 17 | "go.opentelemetry.io/otel/trace/noop" 18 | "go.uber.org/zap" 19 | 20 | "github.com/go-faster/sdk/zctx" 21 | ) 22 | 23 | const ( 24 | expOTLP = "otlp" 25 | expNone = "none" // no-op 26 | 27 | protoHTTP = "http" 28 | protoGRPC = "grpc" 29 | defaultProto = protoGRPC 30 | ) 31 | 32 | const ( 33 | writerStdout = "stdout" 34 | writerStderr = "stderr" 35 | ) 36 | 37 | func writerByName(name string) io.Writer { 38 | switch name { 39 | case writerStdout: 40 | return os.Stdout 41 | case writerStderr: 42 | return os.Stderr 43 | default: 44 | return io.Discard 45 | } 46 | } 47 | 48 | func getEnvOr(name, def string) string { 49 | if v := os.Getenv(name); v != "" { 50 | return v 51 | } 52 | return def 53 | } 54 | 55 | func nop(_ context.Context) error { return nil } 56 | 57 | type ShutdownFunc func(ctx context.Context) error 58 | 59 | func NewTracerProvider(ctx context.Context, options ...Option) ( 60 | tracerProvider trace.TracerProvider, 61 | tracerShutdown ShutdownFunc, 62 | err error, 63 | ) { 64 | cfg := newConfig(options) 65 | lg := zctx.From(ctx) 66 | var traceOptions []sdktrace.TracerProviderOption 67 | if cfg.res != nil { 68 | traceOptions = append(traceOptions, sdktrace.WithResource(cfg.res)) 69 | } 70 | ret := func(e sdktrace.SpanExporter) (trace.TracerProvider, func(ctx context.Context) error, error) { 71 | traceOptions = append(traceOptions, sdktrace.WithBatcher(e)) 72 | return sdktrace.NewTracerProvider(traceOptions...), e.Shutdown, nil 73 | } 74 | 75 | exporter := strings.TrimSpace(getEnvOr("OTEL_TRACES_EXPORTER", expOTLP)) 76 | switch exporter { 77 | case expOTLP: 78 | proto := os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") 79 | if proto == "" { 80 | proto = os.Getenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL") 81 | } 82 | if proto == "" { 83 | proto = defaultProto 84 | } 85 | lg.Debug("Using OTLP trace exporter", zap.String("protocol", proto)) 86 | switch proto { 87 | case protoHTTP: 88 | exp, err := otlptracehttp.New(ctx) 89 | if err != nil { 90 | return nil, nil, errors.Wrap(err, "create OTLP HTTP trace exporter") 91 | } 92 | return ret(exp) 93 | case protoGRPC: 94 | exp, err := otlptracegrpc.New(ctx) 95 | if err != nil { 96 | return nil, nil, errors.Wrap(err, "create OTLP gRPC trace exporter") 97 | } 98 | return ret(exp) 99 | 100 | default: 101 | return nil, nil, errors.Errorf("unsupported traces otlp protocol %q", proto) 102 | } 103 | case writerStdout, writerStderr: 104 | lg.Debug("Using stdout trace exporter", zap.String("writer", exporter)) 105 | writer := cfg.writer 106 | if writer == nil { 107 | writer = writerByName(exporter) 108 | } 109 | exp, err := stdouttrace.New(stdouttrace.WithWriter(writer)) 110 | if err != nil { 111 | return nil, nil, errors.Wrapf(err, "create %q trace exporter", exporter) 112 | } 113 | return ret(exp) 114 | case expNone: 115 | lg.Debug("Using no-op trace exporter") 116 | return noop.NewTracerProvider(), nop, nil 117 | default: 118 | lookup := cfg.lookup 119 | if lookup == nil { 120 | break 121 | } 122 | lg.Debug("Looking for traces exporter", zap.String("exporter", exporter)) 123 | exp, ok, err := lookup(ctx, exporter) 124 | if err != nil { 125 | return nil, nil, errors.Wrapf(err, "create %q", exporter) 126 | } 127 | if !ok { 128 | break 129 | } 130 | 131 | lg.Debug("Using user-defined traces exporter", zap.String("exporter", exporter)) 132 | return ret(exp) 133 | } 134 | return nil, nil, errors.Errorf("unsupported OTEL_TRACES_EXPORTER %q", exporter) 135 | } 136 | -------------------------------------------------------------------------------- /autotracer/config.go: -------------------------------------------------------------------------------- 1 | package autotracer 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "go.opentelemetry.io/otel/sdk/resource" 8 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 9 | ) 10 | 11 | // config contains configuration options for a MeterProvider. 12 | type config struct { 13 | res *resource.Resource 14 | writer io.Writer 15 | lookup LookupExporter 16 | } 17 | 18 | // newConfig returns a config configured with options. 19 | func newConfig(options []Option) config { 20 | conf := config{res: resource.Default()} 21 | for _, o := range options { 22 | conf = o.apply(conf) 23 | } 24 | return conf 25 | } 26 | 27 | // Option applies a configuration option value to a MeterProvider. 28 | type Option interface { 29 | apply(config) config 30 | } 31 | 32 | // optionFunc applies a set of options to a config. 33 | type optionFunc func(config) config 34 | 35 | // apply returns a config with option(s) applied. 36 | func (o optionFunc) apply(conf config) config { 37 | return o(conf) 38 | } 39 | 40 | // WithResource associates a Resource with a MeterProvider. This Resource 41 | // represents the entity producing telemetry and is associated with all Meters 42 | // the MeterProvider will create. 43 | // 44 | // By default, if this Option is not used, the default Resource from the 45 | // go.opentelemetry.io/otel/sdk/resource package will be used. 46 | func WithResource(res *resource.Resource) Option { 47 | return optionFunc(func(conf config) config { 48 | conf.res = res 49 | return conf 50 | }) 51 | } 52 | 53 | // WithWriter sets writer for the stderr, stdout exporters. 54 | func WithWriter(out io.Writer) Option { 55 | return optionFunc(func(conf config) config { 56 | conf.writer = out 57 | return conf 58 | }) 59 | } 60 | 61 | // LookupExporter creates exporter by name. 62 | type LookupExporter func(ctx context.Context, name string) (sdktrace.SpanExporter, bool, error) 63 | 64 | // WithLookupExporter sets exporter lookup function. 65 | func WithLookupExporter(lookup LookupExporter) Option { 66 | return optionFunc(func(conf config) config { 67 | conf.lookup = lookup 68 | return conf 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /autotracer/config_test.go: -------------------------------------------------------------------------------- 1 | package autotracer 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" 12 | "go.opentelemetry.io/otel/sdk/trace" 13 | ) 14 | 15 | func TestWithLookupExporter(t *testing.T) { 16 | var lookup LookupExporter = func(ctx context.Context, name string) (trace.SpanExporter, bool, error) { 17 | switch name { 18 | case "return_something": 19 | e, err := stdouttrace.New(stdouttrace.WithWriter(io.Discard)) 20 | return e, true, err 21 | case "return_error": 22 | return nil, false, errors.New("test error") 23 | default: 24 | return nil, false, nil 25 | } 26 | } 27 | 28 | for i, tt := range []struct { 29 | name string 30 | containsErr string 31 | }{ 32 | {"return_something", ``}, 33 | {"return_error", `test error`}, 34 | {"return_not_exist", `unsupported OTEL_TRACES_EXPORTER "return_not_exist"`}, 35 | } { 36 | tt := tt 37 | t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { 38 | t.Setenv("OTEL_TRACES_EXPORTER", tt.name) 39 | ctx := context.Background() 40 | 41 | _, _, err := NewTracerProvider(ctx, WithLookupExporter(lookup)) 42 | if tt.containsErr != "" { 43 | require.ErrorContains(t, err, tt.containsErr) 44 | return 45 | } 46 | require.NoError(t, err) 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cmd/sdk-example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "go.opentelemetry.io/otel/sdk/resource" 8 | "go.uber.org/zap" 9 | "go.uber.org/zap/zapcore" 10 | 11 | "github.com/go-faster/sdk/app" 12 | "github.com/go-faster/sdk/autometer" 13 | "github.com/go-faster/sdk/autotracer" 14 | ) 15 | 16 | func main() { 17 | app.Run(func(ctx context.Context, lg *zap.Logger, t *app.Telemetry) error { 18 | lg.Info("Hello, world!") 19 | <-t.ShutdownContext().Done() 20 | lg.Info("Goodbye, world!") 21 | return nil 22 | }, 23 | // Configure custom zap config. 24 | app.WithZapTee(false), 25 | app.WithZapConfig(zap.NewDevelopmentConfig()), 26 | app.WithZapOptions( 27 | // Custom zap logger options. 28 | // E.g. hooks, custom core. 29 | zap.WrapCore(func(core zapcore.Core) zapcore.Core { 30 | return zapcore.NewTee(core) 31 | }), 32 | ), 33 | app.WithZapOpenTelemetry(), 34 | 35 | // Redirect metrics and traces to /dev/null. 36 | app.WithMeterOptions(autometer.WithWriter(io.Discard)), 37 | app.WithTracerOptions(autotracer.WithWriter(io.Discard)), 38 | 39 | // Set base context. Background context is used by default. 40 | app.WithContext(context.Background()), 41 | 42 | // Set default service name and namespace. 43 | // Incompatible with [app.WithResource]. 44 | app.WithServiceName("example"), 45 | app.WithServiceNamespace("sdk"), 46 | 47 | // Set default resource options. 48 | app.WithResourceOptions( 49 | resource.WithProcessRuntimeDescription(), 50 | resource.WithProcessRuntimeVersion(), 51 | resource.WithProcessRuntimeName(), 52 | resource.WithOS(), 53 | resource.WithFromEnv(), 54 | resource.WithTelemetrySDK(), 55 | resource.WithHost(), 56 | resource.WithProcess(), 57 | ), 58 | 59 | // Also allows to set custom resource. 60 | app.WithResource(func(ctx context.Context) (*resource.Resource, error) { 61 | return resource.Default(), nil 62 | }), 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export OTEL_TRACES_EXPORTER="none" 4 | export OTEL_METRICS_EXPORTER="none" 5 | export OTEL_LOGS_EXPORTER="stderr" 6 | 7 | go run ./cmd/sdk-example -------------------------------------------------------------------------------- /go.coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | go test -race -v -coverpkg=./... -coverprofile=profile.out ./... 6 | go tool cover -func profile.out 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-faster/sdk 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/KimMachineGun/automemlimit v0.7.2 9 | github.com/go-faster/errors v0.7.1 10 | github.com/go-logr/zapr v1.3.0 11 | github.com/grafana/otel-profiling-go v0.5.1 12 | github.com/grafana/pyroscope-go v1.2.2 13 | github.com/prometheus/client_golang v1.22.0 14 | github.com/samber/slog-zap/v2 v2.6.2 15 | github.com/stretchr/testify v1.10.0 16 | go.opentelemetry.io/collector/pdata v1.32.0 17 | go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 18 | go.opentelemetry.io/contrib/instrumentation/runtime v0.61.0 19 | go.opentelemetry.io/contrib/propagators/autoprop v0.61.0 20 | go.opentelemetry.io/otel v1.36.0 21 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 22 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 23 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 24 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 25 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 26 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 27 | go.opentelemetry.io/otel/exporters/prometheus v0.58.0 28 | go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 29 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 30 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 31 | go.opentelemetry.io/otel/log v0.12.2 32 | go.opentelemetry.io/otel/metric v1.36.0 33 | go.opentelemetry.io/otel/sdk v1.36.0 34 | go.opentelemetry.io/otel/sdk/log v0.12.2 35 | go.opentelemetry.io/otel/sdk/metric v1.36.0 36 | go.opentelemetry.io/otel/trace v1.36.0 37 | go.uber.org/automaxprocs v1.6.0 38 | go.uber.org/zap v1.27.0 39 | golang.org/x/sync v0.14.0 40 | google.golang.org/grpc v1.72.1 41 | ) 42 | 43 | require ( 44 | github.com/beorn7/perks v1.0.1 // indirect 45 | github.com/cenkalti/backoff/v5 v5.0.2 // indirect 46 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 47 | github.com/davecgh/go-spew v1.1.1 // indirect 48 | github.com/go-logr/logr v1.4.2 // indirect 49 | github.com/go-logr/stdr v1.2.2 // indirect 50 | github.com/gogo/protobuf v1.3.2 // indirect 51 | github.com/google/uuid v1.6.0 // indirect 52 | github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect 53 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect 54 | github.com/json-iterator/go v1.1.12 // indirect 55 | github.com/klauspost/compress v1.18.0 // indirect 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 57 | github.com/modern-go/reflect2 v1.0.2 // indirect 58 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 59 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 60 | github.com/pmezard/go-difflib v1.0.0 // indirect 61 | github.com/prometheus/client_model v0.6.2 // indirect 62 | github.com/prometheus/common v0.64.0 // indirect 63 | github.com/prometheus/procfs v0.16.1 // indirect 64 | github.com/samber/lo v1.47.0 // indirect 65 | github.com/samber/slog-common v0.18.1 // indirect 66 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 67 | go.opentelemetry.io/contrib/propagators/aws v1.36.0 // indirect 68 | go.opentelemetry.io/contrib/propagators/b3 v1.36.0 // indirect 69 | go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 // indirect 70 | go.opentelemetry.io/contrib/propagators/ot v1.36.0 // indirect 71 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect 72 | go.opentelemetry.io/otel/log/logtest v0.0.0-20250521132538-355c8ccc2694 // indirect 73 | go.opentelemetry.io/proto/otlp v1.6.0 // indirect 74 | go.uber.org/multierr v1.11.0 // indirect 75 | golang.org/x/net v0.40.0 // indirect 76 | golang.org/x/sys v0.33.0 // indirect 77 | golang.org/x/text v0.25.0 // indirect 78 | google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect 79 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect 80 | google.golang.org/protobuf v1.36.6 // indirect 81 | gopkg.in/yaml.v3 v3.0.1 // indirect 82 | ) 83 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/KimMachineGun/automemlimit v0.7.2 h1:DyfHI7zLWmZPn2Wqdy2AgTiUvrGPmnYWgwhHXtAegX4= 2 | github.com/KimMachineGun/automemlimit v0.7.2/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= 3 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 | github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= 6 | github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 7 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 8 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= 13 | github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= 14 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 15 | github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 16 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 17 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 18 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 19 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 20 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 21 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 22 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 23 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 24 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 25 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 26 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 27 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 28 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 29 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 30 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 31 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 32 | github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= 33 | github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= 34 | github.com/grafana/pyroscope-go v1.2.2 h1:uvKCyZMD724RkaCEMrSTC38Yn7AnFe8S2wiAIYdDPCE= 35 | github.com/grafana/pyroscope-go v1.2.2/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU= 36 | github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= 37 | github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= 38 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= 39 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= 40 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 41 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 42 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 43 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 44 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 45 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 46 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 47 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 48 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 49 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 50 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 51 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 52 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 54 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 55 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 56 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 57 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 58 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 59 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= 60 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= 61 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 62 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 63 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 64 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 65 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 66 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 67 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 68 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 69 | github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= 70 | github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= 71 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 72 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 73 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 74 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 75 | github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= 76 | github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= 77 | github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ= 78 | github.com/samber/slog-common v0.18.1/go.mod h1:QNZiNGKakvrfbJ2YglQXLCZauzkI9xZBjOhWFKS3IKk= 79 | github.com/samber/slog-zap/v2 v2.6.2 h1:IPHgVQjBfEwqu7fBxSxvvl+/E4b7TqAu/eispdQdv9M= 80 | github.com/samber/slog-zap/v2 v2.6.2/go.mod h1:bMOphuaRcThr+2X7vE4kFaqyr1lqGkc9Js95n9X6xaU= 81 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 82 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 83 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 84 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 85 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 86 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 87 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 88 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 89 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 90 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 91 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 92 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 93 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 94 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 95 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 96 | go.opentelemetry.io/collector/pdata v1.32.0 h1:hBzlJV1rujr1UdD2CBy2gmaIKtC15ysg/z+x8F3McQA= 97 | go.opentelemetry.io/collector/pdata v1.32.0/go.mod h1:m41io9nWpy7aCm/uD1L9QcKiZwOP0ldj83JEA34dmlk= 98 | go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 h1:u2E32P7j1a/gRgZDWhIXC+Shd4rLg70mnE7QLI/Ssnw= 99 | go.opentelemetry.io/contrib/bridges/otelzap v0.11.0/go.mod h1:pJPCLM8gzX4ASqLlyAXjHBEYxgbOQJ/9bidWxD6PEPQ= 100 | go.opentelemetry.io/contrib/instrumentation/runtime v0.61.0 h1:oIZsTHd0YcrvvUCN2AaQqyOcd685NQ+rFmrajveCIhA= 101 | go.opentelemetry.io/contrib/instrumentation/runtime v0.61.0/go.mod h1:X4KSPIvxnY/G5c9UOGXtFoL91t1gmlHpDQzeK5Zc/Bw= 102 | go.opentelemetry.io/contrib/propagators/autoprop v0.61.0 h1:cxOVDJ30qfzV27G5p9WMtJUB/3cXC0iL+u9EV1fSOws= 103 | go.opentelemetry.io/contrib/propagators/autoprop v0.61.0/go.mod h1:Y+xiUbWetg65vAroDZcIzJ5wyPNWRH32EoIV9rIaa0g= 104 | go.opentelemetry.io/contrib/propagators/aws v1.36.0 h1:Txhy/1LZIbbnutftc5pdU8Y9vOQuAkuIOFXuLsdDejs= 105 | go.opentelemetry.io/contrib/propagators/aws v1.36.0/go.mod h1:M3A0491jGFPNHU8b3zEW7r/gtsMpGOsFUO3WL+SZ1xw= 106 | go.opentelemetry.io/contrib/propagators/b3 v1.36.0 h1:xrAb/G80z/l5JL6XlmUMSD1i6W8vXkWrLfmkD3w/zZo= 107 | go.opentelemetry.io/contrib/propagators/b3 v1.36.0/go.mod h1:UREJtqioFu5awNaCR8aEx7MfJROFlAWb6lPaJFbHaG0= 108 | go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 h1:SoCgXYF4ISDtNyfLUzsGDaaudZVTx2yJhOyBO0+/GYk= 109 | go.opentelemetry.io/contrib/propagators/jaeger v1.36.0/go.mod h1:VHu48l0YTRKSObdPQ+Sb8xMZvdnJlN7yhHuHoPgNqHM= 110 | go.opentelemetry.io/contrib/propagators/ot v1.36.0 h1:UBoZjbx483GslNKYK2YpfvePTJV4BHGeFd8+b7dexiM= 111 | go.opentelemetry.io/contrib/propagators/ot v1.36.0/go.mod h1:adDDRry19/n9WoA7mSCMjoVJcmzK/bZYzX9SR+g2+W4= 112 | go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= 113 | go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= 114 | go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= 115 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= 116 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY= 117 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 h1:tPLwQlXbJ8NSOfZc4OkgU5h2A38M4c9kfHSVc4PFQGs= 118 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2/go.mod h1:QTnxBwT/1rBIgAG1goq6xMydfYOBKU6KTiYF4fp5zL8= 119 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow= 120 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30= 121 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 h1:gAU726w9J8fwr4qRDqu1GYMNNs4gXrU+Pv20/N1UpB4= 122 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0/go.mod h1:RboSDkp7N292rgu+T0MgVt2qgFGu6qa1RpZDOtpL76w= 123 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= 124 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= 125 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= 126 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= 127 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM= 128 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= 129 | go.opentelemetry.io/otel/exporters/prometheus v0.58.0 h1:CJAxWKFIqdBennqxJyOgnt5LqkeFRT+Mz3Yjz3hL+h8= 130 | go.opentelemetry.io/otel/exporters/prometheus v0.58.0/go.mod h1:7qo/4CLI+zYSNbv0GMNquzuss2FVZo3OYrGh96n4HNc= 131 | go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 h1:12vMqzLLNZtXuXbJhSENRg+Vvx+ynNilV8twBLBsXMY= 132 | go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2/go.mod h1:ZccPZoPOoq8x3Trik/fCsba7DEYDUnN6yX79pgp2BUQ= 133 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= 134 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= 135 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0= 136 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= 137 | go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc= 138 | go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E= 139 | go.opentelemetry.io/otel/log/logtest v0.0.0-20250521132538-355c8ccc2694 h1:JdgdaA8zooYk1NS1yoZVMUQXJ5SP2/ouydK9PBVRuu4= 140 | go.opentelemetry.io/otel/log/logtest v0.0.0-20250521132538-355c8ccc2694/go.mod h1:2lJf2Aeu3FeOruGpon4A3PSamXLCZPWQrJ/THiW2938= 141 | go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= 142 | go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= 143 | go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= 144 | go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= 145 | go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= 146 | go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= 147 | go.opentelemetry.io/otel/sdk/log v0.12.2 h1:yNoETvTByVKi7wHvYS6HMcZrN5hFLD7I++1xIZ/k6W0= 148 | go.opentelemetry.io/otel/sdk/log v0.12.2/go.mod h1:DcpdmUXHJgSqN/dh+XMWa7Vf89u9ap0/AAk/XGLnEzY= 149 | go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0= 150 | go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE= 151 | go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= 152 | go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= 153 | go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= 154 | go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= 155 | go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= 156 | go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= 157 | go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= 158 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 159 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 160 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 161 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 162 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 163 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 164 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 165 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 166 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 167 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 168 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 169 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 170 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 171 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 172 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 173 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 174 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 175 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 176 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 177 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 178 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 179 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 180 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 181 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 182 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 183 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 184 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 185 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 186 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 187 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 188 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 189 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 190 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 191 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 192 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 193 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 194 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 195 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 196 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 197 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 198 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 199 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 200 | google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= 201 | google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= 202 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= 203 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 204 | google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= 205 | google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 206 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 207 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 208 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 209 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 210 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 211 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 212 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 213 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 214 | -------------------------------------------------------------------------------- /go.test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | echo "test" 6 | go test --timeout 5m ./... 7 | 8 | echo "test -race" 9 | go test --timeout 5m -race ./... 10 | -------------------------------------------------------------------------------- /gold/_golden/file.hex: -------------------------------------------------------------------------------- 1 | 00000000 01 02 03 48 69 21 |...Hi!| 2 | -------------------------------------------------------------------------------- /gold/_golden/file.raw: -------------------------------------------------------------------------------- 1 | Hi! -------------------------------------------------------------------------------- /gold/_golden/hello.txt: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /gold/gold.go: -------------------------------------------------------------------------------- 1 | // Package gold implements golden files. 2 | package gold 3 | 4 | import ( 5 | "bytes" 6 | "encoding/hex" 7 | "flag" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | const defaultDir = "_golden" 17 | 18 | // _update reports whether golden files update is requested. 19 | // 20 | // Call Init() in TestMain to propagate. 21 | var _update bool 22 | 23 | // _clean reports whether all golden files should be removed before 24 | // running tests. 25 | // 26 | // Call Init() in TestMain to propagate. 27 | var _clean bool 28 | 29 | // Init should be called in TestMain. 30 | func Init() { 31 | flag.BoolVar(&_update, "update", false, "update golden files") 32 | flag.BoolVar(&_clean, "clean", true, "clean golden files") 33 | flag.Parse() 34 | 35 | if _clean && _update { 36 | dir, err := os.ReadDir(defaultDir) 37 | if err != nil { 38 | // Ignore any error. 39 | return 40 | } 41 | for _, f := range dir { 42 | p := filepath.Join(defaultDir, f.Name()) 43 | if err := os.RemoveAll(p); err != nil { 44 | panic(err) 45 | } 46 | } 47 | } 48 | } 49 | 50 | // filePath returns path to golden file. 51 | func filePath(elems ...string) string { 52 | return filepath.Join( 53 | append([]string{defaultDir}, elems...)..., 54 | ) 55 | } 56 | 57 | func exists(t testing.TB, elems ...string) bool { 58 | t.Helper() 59 | 60 | p := filePath(elems...) 61 | data, err := os.Stat(p) 62 | if err == nil { 63 | if data.IsDir() { 64 | t.Fatalf("golden file %s is directory", p) 65 | } 66 | return true 67 | } 68 | if os.IsNotExist(err) { 69 | return false 70 | } 71 | 72 | // Unexpected error 73 | t.Fatal(err) 74 | return false 75 | } 76 | 77 | // readFile reads golden file. 78 | func readFile(t testing.TB, elems ...string) []byte { 79 | t.Helper() 80 | 81 | p := filePath(elems...) 82 | data, err := os.ReadFile(p) // nolint:gosec // testing 83 | if err != nil { 84 | t.Fatalf("golden file %s: %+v", path.Join(elems...), err) 85 | } 86 | 87 | return data 88 | } 89 | 90 | func writeFile(t testing.TB, data []byte, elems ...string) { 91 | t.Helper() 92 | 93 | p := filePath(elems...) 94 | require.NoError(t, os.MkdirAll(path.Dir(p), 0o700), "make dir for golden files") 95 | require.NoError(t, os.WriteFile(p, data, 0o600), "write golden file") 96 | } 97 | 98 | // NormalizeNewlines normalizes \r\n (windows) and \r (mac) 99 | // into \n (unix). 100 | func NormalizeNewlines(s string) string { 101 | return string(normalizeNewlines([]byte(s))) 102 | } 103 | 104 | // normalizeNewlines normalizes \r\n (windows) and \r (mac) 105 | // into \n (unix). 106 | func normalizeNewlines(d []byte) []byte { 107 | // replace CR LF \r\n (windows) with LF \n (unix) 108 | d = bytes.ReplaceAll(d, []byte{13, 10}, []byte{10}) 109 | // replace CF \r (mac) with LF \n (unix) 110 | d = bytes.ReplaceAll(d, []byte{13}, []byte{10}) 111 | return d 112 | } 113 | 114 | // Str checks text golden file. 115 | func Str(t testing.TB, s string, name ...string) { 116 | t.Helper() 117 | 118 | if len(name) == 0 { 119 | name = []string{"file.txt"} 120 | } 121 | 122 | update := _update 123 | if !exists(t, name...) { 124 | t.Log("Populating initial golden file") 125 | update = true 126 | } 127 | if update { 128 | writeFile(t, []byte(s), name...) 129 | } 130 | 131 | data := readFile(t, name...) 132 | data = normalizeNewlines(data) 133 | 134 | require.Equal(t, string(data), s, "golden file text mismatch") 135 | } 136 | 137 | // Bytes check binary golden file. 138 | func Bytes(t testing.TB, data []byte, name ...string) { 139 | t.Helper() 140 | 141 | if len(name) == 0 { 142 | name = []string{"file"} 143 | } 144 | 145 | // Adding ".raw" prefix to visually distinguish hex and raw. 146 | last := len(name) - 1 147 | rawName := append([]string{}, name...) 148 | rawName[last] += ".raw" 149 | 150 | update := _update 151 | if !exists(t, rawName...) { 152 | t.Log("Populating initial golden file") 153 | update = true 154 | } 155 | if update { 156 | // Writing hex dump next to raw binary to make 157 | // git diff more understandable on golden file 158 | // updates. 159 | dump := hex.Dump(data) 160 | dumpName := append([]string{}, name...) 161 | dumpName[last] += ".hex" 162 | writeFile(t, []byte(dump), dumpName...) 163 | 164 | // Writing raw file. 165 | writeFile(t, data, rawName...) 166 | } 167 | 168 | expected := readFile(t, rawName...) 169 | require.Equal(t, expected, data, "golden file binary mismatch") 170 | } 171 | -------------------------------------------------------------------------------- /gold/gold_test.go: -------------------------------------------------------------------------------- 1 | package gold_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/go-faster/sdk/gold" 10 | ) 11 | 12 | func TestStr(t *testing.T) { 13 | gold.Str(t, "Hello, world!\n", "hello.txt") 14 | } 15 | 16 | func TestBytes(t *testing.T) { 17 | gold.Bytes(t, append([]byte{1, 2, 3}, "Hi!"...)) 18 | } 19 | 20 | func TestNormalize(t *testing.T) { 21 | 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}" 22 | 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}" 23 | 24 | out := gold.NormalizeNewlines(raw) 25 | 26 | require.Equal(t, normalized, out) 27 | } 28 | 29 | func TestMain(m *testing.M) { 30 | // Explicitly registering flags for golden files. 31 | gold.Init() 32 | 33 | os.Exit(m.Run()) 34 | } 35 | -------------------------------------------------------------------------------- /gold/gotd_private_test.go: -------------------------------------------------------------------------------- 1 | package gold 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestNormalize(t *testing.T) { 10 | 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}" 11 | 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}" 12 | 13 | out := normalizeNewlines([]byte(raw)) 14 | 15 | require.Equal(t, normalized, string(out)) 16 | } 17 | -------------------------------------------------------------------------------- /otelenv/env.go: -------------------------------------------------------------------------------- 1 | // Package otelenv provides helpers for working with OTEL_RESOURCE_ATTRIBUTES. 2 | package otelenv 3 | 4 | import ( 5 | "os" 6 | "strings" 7 | 8 | "go.opentelemetry.io/otel/attribute" 9 | ) 10 | 11 | // Value of OTEL_RESOURCE_ATTRIBUTES for key value list. 12 | func Value(values ...attribute.KeyValue) string { 13 | var parts []string 14 | for _, kv := range values { 15 | parts = append(parts, string(kv.Key)+"="+kv.Value.AsString()) 16 | } 17 | return strings.Join(parts, ",") 18 | } 19 | 20 | func Set(values ...attribute.KeyValue) { 21 | _ = os.Setenv("OTEL_RESOURCE_ATTRIBUTES", Value(values...)) 22 | } 23 | -------------------------------------------------------------------------------- /otelsync/adapter.go: -------------------------------------------------------------------------------- 1 | package otelsync 2 | 3 | import ( 4 | "context" 5 | 6 | "go.opentelemetry.io/otel/metric" 7 | ) 8 | 9 | // Adapter provides a sync adapter over async metric instruments. 10 | type Adapter struct { 11 | meter metric.Meter 12 | gauge []*GaugeInt64 13 | } 14 | 15 | func (a *Adapter) callback(_ context.Context, o metric.Observer) error { 16 | for _, v := range a.gauge { 17 | v.observe(o) 18 | } 19 | return nil 20 | } 21 | 22 | // Register registers callback. 23 | func (a *Adapter) Register() (metric.Registration, error) { 24 | var in []metric.Observable 25 | for _, v := range a.gauge { 26 | in = append(in, v.Int64ObservableGauge) 27 | } 28 | return a.meter.RegisterCallback(a.callback, in...) 29 | } 30 | 31 | // GaugeInt64 returns a new sync int64 gauge. Register must be called after creating all gauges. 32 | func (a *Adapter) GaugeInt64(name string, options ...metric.Int64ObservableGaugeOption) (metric.Int64Observer, error) { 33 | og, err := a.meter.Int64ObservableGauge(name, options...) 34 | if err != nil { 35 | return nil, err 36 | } 37 | g := &GaugeInt64{ 38 | Int64ObservableGauge: og, 39 | } 40 | a.gauge = append(a.gauge, g) 41 | return g, nil 42 | } 43 | 44 | func NewAdapter(m metric.Meter) *Adapter { 45 | a := &Adapter{ 46 | meter: m, 47 | } 48 | 49 | return a 50 | } 51 | -------------------------------------------------------------------------------- /otelsync/gauge.go: -------------------------------------------------------------------------------- 1 | package otelsync 2 | 3 | import ( 4 | "sync" 5 | 6 | "go.opentelemetry.io/otel/attribute" 7 | "go.opentelemetry.io/otel/metric" 8 | "go.opentelemetry.io/otel/metric/embedded" 9 | ) 10 | 11 | // GaugeInt64 is a wrapper around metric.Int64ObservableGauge that stores last value 12 | // for each attribute set, providing a sync adapter over async gauge. 13 | type GaugeInt64 struct { 14 | metric.Int64ObservableGauge 15 | embedded.Int64Observer 16 | 17 | mux sync.Mutex 18 | values map[attribute.Set]int64 19 | } 20 | 21 | // Observe records a last value for attribute set. 22 | func (g *GaugeInt64) Observe(v int64, options ...metric.ObserveOption) { 23 | g.mux.Lock() 24 | defer g.mux.Unlock() 25 | 26 | if g.values == nil { 27 | g.values = make(map[attribute.Set]int64) 28 | } 29 | 30 | g.values[metric.NewObserveConfig(options).Attributes()] = v 31 | } 32 | 33 | func (g *GaugeInt64) observe(o metric.Observer) { 34 | g.mux.Lock() 35 | defer g.mux.Unlock() 36 | 37 | for k, v := range g.values { 38 | o.ObserveInt64(g.Int64ObservableGauge, v, metric.WithAttributes(k.ToSlice()...)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /profiler/profiler.go: -------------------------------------------------------------------------------- 1 | // Package profiler implements pprof routes. 2 | package profiler 3 | 4 | import ( 5 | "net/http" 6 | "net/http/pprof" 7 | "path" 8 | runtime "runtime/pprof" 9 | "strings" 10 | ) 11 | 12 | type handler struct { 13 | mux *http.ServeMux 14 | } 15 | 16 | var _ http.Handler = handler{} 17 | 18 | func (p handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 19 | p.mux.ServeHTTP(w, r) 20 | } 21 | 22 | var _defaultRoutes = DefaultRoutes() 23 | 24 | // DefaultRoutes returns default routes. 25 | // 26 | // Route name is "/debug/pprof/". 27 | func DefaultRoutes() []string { 28 | // Enable all routes by default except cmdline (unsafe). 29 | return []string{ 30 | // From pprof.. 31 | "profile", 32 | "symbol", 33 | "trace", 34 | 35 | // From pprof.Handler(). 36 | "goroutine", 37 | "heap", 38 | "threadcreate", 39 | "block", 40 | } 41 | } 42 | 43 | // Options for New. 44 | type Options struct { 45 | Routes []string // defaults to DefaultRoutes 46 | UnknownRoute func(route string) // defaults to ignore 47 | } 48 | 49 | // New returns new pprof handler. 50 | func New(opt Options) http.Handler { 51 | m := http.NewServeMux() 52 | m.HandleFunc("/debug/pprof/", pprof.Index) 53 | routes := opt.Routes 54 | if len(routes) == 0 { 55 | routes = _defaultRoutes 56 | } 57 | unknown := opt.UnknownRoute 58 | if unknown == nil { 59 | unknown = func(route string) {} 60 | } 61 | for _, name := range routes { 62 | name = strings.TrimSpace(name) 63 | route := path.Join("/debug/pprof/", name) 64 | switch name { 65 | case "cmdline": 66 | m.HandleFunc(route, pprof.Cmdline) 67 | case "profile": 68 | m.HandleFunc(route, pprof.Profile) 69 | case "symbol": 70 | m.HandleFunc(route, pprof.Symbol) 71 | case "trace": 72 | m.HandleFunc(route, pprof.Trace) 73 | default: 74 | if runtime.Lookup(name) == nil { 75 | unknown(name) 76 | continue 77 | } 78 | m.Handle(route, pprof.Handler(name)) 79 | } 80 | } 81 | return handler{mux: m} 82 | } 83 | -------------------------------------------------------------------------------- /profiler/profiler_test.go: -------------------------------------------------------------------------------- 1 | package profiler 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestNew(t *testing.T) { 12 | unknownRoutes := []string{ 13 | "foo", "bar", 14 | } 15 | routes := []string{ 16 | // From pprof.. 17 | "profile", 18 | "symbol", 19 | "trace", 20 | 21 | // From pprof.Handler(). 22 | "goroutine", 23 | "heap", 24 | "threadcreate", 25 | "block", 26 | } 27 | var called []string 28 | h := New(Options{ 29 | Routes: append(routes, unknownRoutes...), 30 | UnknownRoute: func(route string) { 31 | called = append(called, route) 32 | }, 33 | }) 34 | require.NotNil(t, h) 35 | require.Equal(t, unknownRoutes, called) 36 | } 37 | 38 | func TestHandler_ServeHTTP(t *testing.T) { 39 | h := New(Options{}) 40 | require.NotNil(t, h) 41 | s := httptest.NewServer(h) 42 | t.Cleanup(s.Close) 43 | t.Run("Found", func(t *testing.T) { 44 | for _, v := range []string{ 45 | "/debug/pprof", 46 | "/debug/pprof/symbol", 47 | "/debug/pprof/goroutine", 48 | } { 49 | req, err := http.NewRequest(http.MethodGet, s.URL+v, http.NoBody) 50 | require.NoError(t, err) 51 | 52 | res, err := s.Client().Do(req) 53 | require.NoErrorf(t, err, "request: %s", req.URL) 54 | require.Equalf(t, http.StatusOK, res.StatusCode, "%s: %s", v, res.Status) 55 | } 56 | }) 57 | t.Run("NotFound", func(t *testing.T) { 58 | for _, v := range []string{ 59 | "/", 60 | "/debug/pprof/foo", 61 | "/debug/pprof/cmdline", 62 | } { 63 | req, err := http.NewRequest(http.MethodGet, s.URL+v, http.NoBody) 64 | require.NoError(t, err) 65 | 66 | res, err := s.Client().Do(req) 67 | require.NoErrorf(t, err, "request: %s", req.URL) 68 | require.Equalf(t, http.StatusNotFound, res.StatusCode, "%s: %s (should be not found)", v, res.Status) 69 | } 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /race/race.go: -------------------------------------------------------------------------------- 1 | // Package race detects -race compile flag. 2 | package race 3 | -------------------------------------------------------------------------------- /race/race_off.go: -------------------------------------------------------------------------------- 1 | //go:build !race 2 | 3 | package race 4 | 5 | // Enabled is false. 6 | const Enabled = false 7 | -------------------------------------------------------------------------------- /race/race_on.go: -------------------------------------------------------------------------------- 1 | //go:build race 2 | 3 | package race 4 | 5 | // Enabled is true. 6 | const Enabled = true 7 | -------------------------------------------------------------------------------- /race/race_on_test.go: -------------------------------------------------------------------------------- 1 | //go:build race 2 | 3 | package race 4 | 5 | import "testing" 6 | 7 | func TestRaceOn(t *testing.T) { 8 | Skip(t) 9 | t.Fatal("Should be skipped") 10 | } 11 | -------------------------------------------------------------------------------- /race/skip.go: -------------------------------------------------------------------------------- 1 | package race 2 | 3 | import "testing" 4 | 5 | // Skip if race enabled. 6 | func Skip(t *testing.T) { 7 | t.Helper() 8 | if Enabled { 9 | t.Skip("Skipping: -race enabled") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /zapotel/zapotel.go: -------------------------------------------------------------------------------- 1 | // Package zapotel provides OpenTelemetry logs exporter zap core implementation. 2 | // 3 | // Deprecated. Use go.opentelemetry.io/contrib/bridges/otelzap. 4 | package zapotel 5 | 6 | import ( 7 | "context" 8 | "encoding/hex" 9 | "fmt" 10 | "math" 11 | "reflect" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/go-faster/errors" 17 | "go.opentelemetry.io/collector/pdata/pcommon" 18 | "go.opentelemetry.io/collector/pdata/plog" 19 | "go.opentelemetry.io/collector/pdata/plog/plogotlp" 20 | "go.opentelemetry.io/otel/attribute" 21 | "go.opentelemetry.io/otel/sdk/resource" 22 | "go.uber.org/zap/zapcore" 23 | ) 24 | 25 | // New initializes new zapcore.Core from grpc client and resource. 26 | func New(enab zapcore.LevelEnabler, res *resource.Resource, client plogotlp.GRPCClient) zapcore.Core { 27 | return &exporter{ 28 | LevelEnabler: enab, 29 | 30 | sender: &sender{ 31 | client: client, 32 | res: res, 33 | logs: plog.NewLogs(), 34 | rate: time.Second * 1, 35 | maxBatch: 5000, 36 | }, 37 | } 38 | } 39 | 40 | type sender struct { 41 | client plogotlp.GRPCClient 42 | res *resource.Resource 43 | logs plog.Logs 44 | rate time.Duration 45 | maxBatch int 46 | mux sync.Mutex 47 | sent time.Time 48 | } 49 | 50 | func (s *sender) append(ent zapcore.Entry, fields []zapcore.Field) { 51 | // https://github.com/open-telemetry/oteps/blob/main/text/logs/0097-log-data-model.md#zap 52 | rl := s.logs.ResourceLogs().AppendEmpty() 53 | { 54 | a := rl.Resource().Attributes() 55 | for _, kv := range s.res.Attributes() { 56 | k := string(kv.Key) 57 | switch kv.Value.Type() { 58 | case attribute.STRING: 59 | a.PutStr(k, kv.Value.AsString()) 60 | case attribute.BOOL: 61 | a.PutBool(k, kv.Value.AsBool()) 62 | default: 63 | a.PutStr(k, kv.Value.AsString()) 64 | } 65 | } 66 | } 67 | 68 | il := rl.ScopeLogs().AppendEmpty() 69 | 70 | scope := il.Scope() 71 | scope.SetName("zapotel") 72 | scope.SetVersion("v0.1") 73 | 74 | lg := il.LogRecords().AppendEmpty() 75 | lg.Body().SetStr(ent.Message) 76 | // TODO: update mapping from spec 77 | switch ent.Level { 78 | case zapcore.DebugLevel: 79 | lg.SetSeverityNumber(plog.SeverityNumberDebug) 80 | case zapcore.InfoLevel: 81 | lg.SetSeverityNumber(plog.SeverityNumberInfo) 82 | case zapcore.WarnLevel: 83 | lg.SetSeverityNumber(plog.SeverityNumberWarn) 84 | case zapcore.ErrorLevel: 85 | lg.SetSeverityNumber(plog.SeverityNumberError) 86 | case zapcore.DPanicLevel: 87 | lg.SetSeverityNumber(plog.SeverityNumberFatal) 88 | case zapcore.PanicLevel: 89 | lg.SetSeverityNumber(plog.SeverityNumberFatal) 90 | case zapcore.FatalLevel: 91 | lg.SetSeverityNumber(plog.SeverityNumberFatal) 92 | } 93 | lg.SetSeverityText(ent.Level.String()) 94 | lg.SetTimestamp(pcommon.NewTimestampFromTime(ent.Time)) 95 | lg.SetObservedTimestamp(pcommon.NewTimestampFromTime(ent.Time)) 96 | { 97 | a := lg.Attributes() 98 | if ent.Caller.Defined { 99 | a.PutStr("caller", ent.Caller.TrimmedPath()) 100 | } 101 | if ent.Stack != "" { 102 | a.PutStr("stack", ent.Stack) 103 | } 104 | if ent.LoggerName != "" { 105 | a.PutStr("logger", ent.LoggerName) 106 | } 107 | var skipped uint32 108 | for _, f := range fields { 109 | k := f.Key 110 | switch f.Type { 111 | case zapcore.BoolType: 112 | a.PutBool(k, f.Integer == 1) 113 | case zapcore.StringType: 114 | l := len(f.String) 115 | if (k == "trace_id" && l == 32) || (k == "span_id" && l == 16) { 116 | // Checking for tracing. 117 | var ( 118 | traceID pcommon.TraceID 119 | spanID pcommon.SpanID 120 | ) 121 | v, err := hex.DecodeString(strings.ToLower(f.String)) 122 | if err == nil { 123 | switch k { 124 | case "trace_id": 125 | copy(traceID[:], v) 126 | lg.SetTraceID(traceID) 127 | case "span_id": 128 | copy(spanID[:], v) 129 | lg.SetSpanID(spanID) 130 | } 131 | // Don't add as regular string. 132 | continue 133 | } 134 | } 135 | a.PutStr(k, f.String) 136 | case zapcore.Int8Type, zapcore.Int16Type, zapcore.Int32Type, zapcore.Int64Type, 137 | zapcore.Uint8Type, zapcore.Uint16Type, zapcore.Uint32Type, zapcore.Uint64Type: 138 | a.PutInt(k, f.Integer) 139 | case zapcore.Float32Type: 140 | a.PutDouble(k, float64(math.Float32frombits(uint32(f.Integer)))) 141 | case zapcore.Float64Type: 142 | a.PutDouble(k, math.Float64frombits(uint64(f.Integer))) 143 | case zapcore.TimeType: 144 | a.PutInt(f.Key, f.Integer) 145 | case zapcore.TimeFullType: 146 | a.PutStr(k, f.Interface.(time.Time).Format(time.RFC3339Nano)) 147 | case zapcore.ErrorType: 148 | encodeError(a, k, f.Interface.(error)) 149 | case zapcore.DurationType: 150 | a.PutDouble(k, time.Duration(f.Integer).Seconds()) 151 | default: 152 | // "Any", ... 153 | skipped++ 154 | } 155 | } 156 | if skipped > 0 { 157 | scope.SetDroppedAttributesCount(skipped) 158 | } 159 | } 160 | } 161 | 162 | func (s *sender) send(ctx context.Context) error { 163 | req := plogotlp.NewExportRequestFromLogs(s.logs) 164 | if _, err := s.client.Export(ctx, req); err != nil { 165 | return errors.Wrap(err, "send logs") 166 | } 167 | s.logs = plog.NewLogs() 168 | s.sent = time.Now() 169 | return nil 170 | } 171 | 172 | func (s *sender) Flush(ctx context.Context) error { 173 | s.mux.Lock() 174 | defer s.mux.Unlock() 175 | 176 | if s.logs.LogRecordCount() < 1 { 177 | // Nothing to send. 178 | return nil 179 | } 180 | return s.send(ctx) 181 | } 182 | 183 | func (s *sender) Send(ctx context.Context, ent zapcore.Entry, fields []zapcore.Field) error { 184 | s.mux.Lock() 185 | defer s.mux.Unlock() 186 | 187 | s.append(ent, fields) 188 | if time.Since(s.sent) > s.rate || s.logs.LogRecordCount() >= s.maxBatch { 189 | return s.send(ctx) 190 | } 191 | return nil 192 | } 193 | 194 | type exporter struct { 195 | zapcore.LevelEnabler 196 | context []zapcore.Field 197 | sender *sender 198 | } 199 | 200 | var ( 201 | _ zapcore.Core = (*exporter)(nil) 202 | _ zapcore.LevelEnabler = (*exporter)(nil) 203 | ) 204 | 205 | func (e *exporter) Level() zapcore.Level { 206 | return zapcore.LevelOf(e.LevelEnabler) 207 | } 208 | 209 | func (e *exporter) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { 210 | if e.Enabled(ent.Level) { 211 | return ce.AddCore(ent, e) 212 | } 213 | return ce 214 | } 215 | 216 | func (e *exporter) With(fields []zapcore.Field) zapcore.Core { 217 | return &exporter{ 218 | LevelEnabler: e.LevelEnabler, 219 | 220 | context: append(e.context[:len(e.context):len(e.context)], fields...), 221 | sender: e.sender, 222 | } 223 | } 224 | 225 | func encodeError(a pcommon.Map, key string, err error) { 226 | // TODO: update mapping from spec 227 | 228 | // Try to capture panics (from nil references or otherwise) when calling 229 | // the Error() method 230 | defer func() { 231 | if rerr := recover(); rerr != nil { 232 | // If it's a nil pointer, just say "". The likeliest causes are a 233 | // error that fails to guard against nil or a nil pointer for a 234 | // value receiver, and in either case, "" is a nice result. 235 | if v := reflect.ValueOf(err); v.Kind() == reflect.Ptr && v.IsNil() { 236 | a.PutStr(key, "") 237 | } 238 | } 239 | }() 240 | 241 | basic := err.Error() 242 | a.PutStr(key, basic) 243 | 244 | switch e := err.(type) { 245 | case interface{ Errors() []error }: 246 | for i, v := range e.Errors() { 247 | k := fmt.Sprintf("%s.%d", key, i) 248 | a.PutStr(k, v.Error()) 249 | } 250 | case fmt.Formatter: 251 | verbose := fmt.Sprintf("%+v", e) 252 | if verbose != basic { 253 | // This is a rich error type, like those produced by 254 | // github.com/pkg/errors. 255 | a.PutStr(key+".verbose", verbose) 256 | } 257 | } 258 | } 259 | 260 | func (e *exporter) Write(ent zapcore.Entry, fields []zapcore.Field) error { 261 | all := make([]zapcore.Field, 0, len(fields)+len(e.context)) 262 | all = append(all, e.context...) 263 | all = append(all, fields...) 264 | return e.sender.Send(context.Background(), ent, all) 265 | } 266 | 267 | func (e *exporter) Sync() error { 268 | return e.sender.Flush(context.Background()) 269 | } 270 | -------------------------------------------------------------------------------- /zapotel/zapotel_test.go: -------------------------------------------------------------------------------- 1 | package zapotel 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "go.opentelemetry.io/collector/pdata/plog" 10 | "go.opentelemetry.io/collector/pdata/plog/plogotlp" 11 | "go.opentelemetry.io/otel/sdk/resource" 12 | "go.uber.org/zap" 13 | "go.uber.org/zap/zapcore" 14 | "google.golang.org/grpc" 15 | ) 16 | 17 | type mockClient struct { 18 | logs plog.LogRecordSlice 19 | plogotlp.GRPCClient 20 | } 21 | 22 | func (c *mockClient) Export(_ context.Context, request plogotlp.ExportRequest, _ ...grpc.CallOption) (plogotlp.ExportResponse, error) { 23 | resLogs := request.Logs().ResourceLogs() 24 | for i := 0; i < resLogs.Len(); i++ { 25 | scopeLogs := resLogs.At(i).ScopeLogs() 26 | for i := 0; i < scopeLogs.Len(); i++ { 27 | records := scopeLogs.At(i).LogRecords() 28 | for i := 0; i < records.Len(); i++ { 29 | records.At(i).CopyTo(c.logs.AppendEmpty()) 30 | } 31 | } 32 | } 33 | return plogotlp.NewExportResponse(), nil 34 | } 35 | 36 | func TestLogger(t *testing.T) { 37 | a := require.New(t) 38 | 39 | mock := &mockClient{ 40 | logs: plog.NewLogRecordSlice(), 41 | } 42 | core := New(zapcore.InfoLevel, resource.Empty(), mock) 43 | logger := zap.New(core).With( 44 | zap.Bool("test", true), 45 | ) 46 | 47 | logger.Debug("debug message") 48 | logger.Info("info message", 49 | zap.String("trace_id", "4bf92f3577b34da6a3ce929d0e0e4736"), 50 | zap.String("span_id", "00f067aa0ba902b7"), 51 | ) 52 | logger.Named("warner").Warn("warn message") 53 | logger.Error("error message", zap.Error(errors.New("test error"))) 54 | 55 | // zapotel would send first record immediately. 56 | a.Equal(1, mock.logs.Len()) 57 | a.NoError(core.Sync()) 58 | a.Equal(3, mock.logs.Len()) 59 | 60 | for i, expect := range []struct { 61 | message string 62 | severity plog.SeverityNumber 63 | traceID string 64 | spanID string 65 | attributes map[string]any 66 | }{ 67 | { 68 | "info message", 69 | plog.SeverityNumberInfo, 70 | "4bf92f3577b34da6a3ce929d0e0e4736", 71 | "00f067aa0ba902b7", 72 | map[string]any{ 73 | "test": true, 74 | }, 75 | }, 76 | { 77 | "warn message", 78 | plog.SeverityNumberWarn, 79 | "", 80 | "", 81 | map[string]any{ 82 | "test": true, 83 | "logger": "warner", 84 | }, 85 | }, 86 | { 87 | "error message", 88 | plog.SeverityNumberError, 89 | "", 90 | "", 91 | map[string]any{ 92 | "test": true, 93 | "error": "test error", 94 | }, 95 | }, 96 | } { 97 | record := mock.logs.At(i) 98 | a.Equal(expect.message, record.Body().AsString()) 99 | a.Equal(expect.severity, record.SeverityNumber()) 100 | a.Equal(expect.traceID, record.TraceID().String()) 101 | a.Equal(expect.spanID, record.SpanID().String()) 102 | a.Equal(expect.attributes, record.Attributes().AsRaw()) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /zctx/zctx.go: -------------------------------------------------------------------------------- 1 | // Package zctx is a context-aware zap logger. 2 | package zctx 3 | 4 | import ( 5 | "context" 6 | 7 | "go.opentelemetry.io/otel/trace" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | type key struct{} 12 | 13 | var _nop = zap.NewNop() 14 | 15 | type logger struct { 16 | // Base logger, should not contain span_id and trace_id fields. 17 | base *zap.Logger 18 | 19 | // Span-scoped logger that caches span_id and trace_id fields. 20 | // 21 | // Will be returned by From(ctx) if ctx contains the same span. 22 | lg *zap.Logger 23 | span trace.SpanContext 24 | ctx context.Context 25 | } 26 | 27 | func (l *logger) SetSpan(ctx context.Context, s trace.SpanContext) { 28 | l.span = s 29 | if ctx.Value(otelzapKey{}) != nil { 30 | l.ctx = ctx 31 | l.lg = l.base.With( 32 | zap.Any("ctx", ctx), 33 | ) 34 | } else { 35 | l.lg = l.base.With( 36 | zap.String("span_id", s.SpanID().String()), 37 | zap.String("trace_id", s.TraceID().String()), 38 | ) 39 | } 40 | } 41 | 42 | func from(ctx context.Context) logger { 43 | v, ok := ctx.Value(key{}).(logger) 44 | if !ok { 45 | return logger{base: _nop} 46 | } 47 | return v 48 | } 49 | 50 | // Start allocates new span logger and returns new context with it. 51 | // Use Start to reduce allocations during From, caching the span-scoped logger. 52 | // 53 | // Should be same as ctx = With(ctx), but more effective. 54 | func Start(ctx context.Context) (context.Context, *zap.Logger) { 55 | v := from(ctx) 56 | s := trace.SpanContextFromContext(ctx) 57 | if s.Equal(v.span) { 58 | return ctx, v.lg 59 | } 60 | if !s.IsValid() { 61 | return ctx, v.lg 62 | } 63 | 64 | v.SetSpan(ctx, s) 65 | return context.WithValue(ctx, key{}, v), v.lg 66 | } 67 | 68 | // From returns zap.Logger from context. 69 | func From(ctx context.Context) *zap.Logger { 70 | v := from(ctx) 71 | s := trace.SpanContextFromContext(ctx) 72 | if v.lg != nil && s.Equal(v.span) { 73 | return v.lg 74 | } 75 | if !s.IsValid() { 76 | return v.base 77 | } 78 | v.SetSpan(ctx, s) 79 | return v.lg 80 | } 81 | 82 | func with(ctx context.Context, v logger) context.Context { 83 | return context.WithValue(ctx, key{}, v) 84 | } 85 | 86 | // With returns new context with provided zap fields. 87 | // 88 | // The span and trace IDs must not be added to the base logger because zap 89 | // can't update or replace fields. 90 | func With(ctx context.Context, fields ...zap.Field) context.Context { 91 | v := from(ctx) 92 | v.base = v.base.With(fields...) 93 | 94 | // Check that cached logger is from current span. 95 | s := trace.SpanContextFromContext(ctx) 96 | if v.lg != nil && s.Equal(v.span) { 97 | // Same span, updating cached logger with new fields. 98 | v.lg = v.lg.With(fields...) 99 | } else if s.IsValid() { 100 | // New span. Caching logger. 101 | // 102 | // Next call to From in same span 103 | // will return cached logger. 104 | v.SetSpan(ctx, s) 105 | } else { 106 | // Not in span anymore. 107 | v.lg = v.base 108 | v.span = s 109 | } 110 | 111 | return with(ctx, v) 112 | } 113 | 114 | // Base initializes root logger for using as a base context. Should be done early. 115 | // 116 | // The span and trace IDs must not be added to the base logger because zap 117 | // can't update or replace fields. 118 | func Base(ctx context.Context, lg *zap.Logger) context.Context { 119 | if lg == nil { 120 | lg = _nop 121 | } 122 | return with(ctx, logger{base: lg}) 123 | } 124 | 125 | type otelzapKey struct{} 126 | 127 | // WithOpenTelemetryZap enables otelzap mode, disabling writing span and trace IDs to logs and 128 | // adding ctx as a log field instead. 129 | func WithOpenTelemetryZap(ctx context.Context) context.Context { 130 | return context.WithValue(ctx, otelzapKey{}, struct{}{}) 131 | } 132 | -------------------------------------------------------------------------------- /zctx/zctx_bench_test.go: -------------------------------------------------------------------------------- 1 | package zctx 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "go.opentelemetry.io/otel/trace" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | func BenchmarkWith(b *testing.B) { 12 | b.ReportAllocs() 13 | 14 | ctx := Base(context.Background(), zap.NewNop()) 15 | 16 | f := zap.Int("foo", 1) 17 | 18 | for i := 0; i < b.N; i++ { 19 | c := With(ctx, f) 20 | _ = c.Done 21 | } 22 | } 23 | 24 | func BenchmarkFrom(b *testing.B) { 25 | ctx := Base(context.Background(), zap.NewNop()) 26 | 27 | b.Run("Raw", func(b *testing.B) { 28 | b.ReportAllocs() 29 | b.ResetTimer() 30 | 31 | for i := 0; i < b.N; i++ { 32 | lg := From(ctx) 33 | _ = lg.Sugar 34 | } 35 | }) 36 | b.Run("TracedFresh", func(b *testing.B) { 37 | b.ReportAllocs() 38 | 39 | tracer := newTestTracer() 40 | ctx, span := tracer.Start(ctx, "test") 41 | defer span.End() 42 | 43 | b.ResetTimer() 44 | for i := 0; i < b.N; i++ { 45 | lg := From(ctx) 46 | _ = lg.Sugar 47 | } 48 | }) 49 | b.Run("TracedStarted", func(b *testing.B) { 50 | b.ReportAllocs() 51 | 52 | tracer := newTestTracer() 53 | ctx, span := tracer.Start(ctx, "test") 54 | defer span.End() 55 | 56 | ctx, lg := Start(ctx) 57 | useLogger(lg) 58 | 59 | b.ResetTimer() 60 | for i := 0; i < b.N; i++ { 61 | useLogger(From(ctx)) 62 | } 63 | }) 64 | b.Run("TracedWith", func(b *testing.B) { 65 | b.ReportAllocs() 66 | 67 | tracer := newTestTracer() 68 | ctx, span := tracer.Start(ctx, "test") 69 | defer span.End() 70 | 71 | ctx = With(ctx, zap.Int("foo", 1)) 72 | 73 | b.ResetTimer() 74 | for i := 0; i < b.N; i++ { 75 | useLogger(From(ctx)) 76 | } 77 | }) 78 | } 79 | 80 | func useLogger(lg *zap.Logger) { 81 | _ = lg.Sugar 82 | } 83 | 84 | func BenchmarkTraceFields(b *testing.B) { 85 | ctx := context.Background() 86 | lg := zap.NewNop() 87 | 88 | b.Run("Prepared", func(b *testing.B) { 89 | b.ReportAllocs() 90 | tracer := newTestTracer() 91 | 92 | ctx, span := tracer.Start(ctx, "test") 93 | defer span.End() 94 | s := trace.SpanContextFromContext(ctx) 95 | traceIDField := zap.String("trace_id", s.TraceID().String()) 96 | spanIDField := zap.String("span_id", s.SpanID().String()) 97 | 98 | b.ResetTimer() 99 | for i := 0; i < b.N; i++ { 100 | if v := trace.SpanContextFromContext(ctx); v.Equal(s) { 101 | nlg := lg.With(traceIDField, spanIDField) 102 | useLogger(nlg) 103 | } else { 104 | panic("?") 105 | } 106 | } 107 | }) 108 | b.Run("Fresh", func(b *testing.B) { 109 | b.ReportAllocs() 110 | tracer := newTestTracer() 111 | 112 | ctx, span := tracer.Start(ctx, "test") 113 | defer span.End() 114 | 115 | b.ResetTimer() 116 | for i := 0; i < b.N; i++ { 117 | s := trace.SpanContextFromContext(ctx) 118 | nlg := lg.With( 119 | zap.String("trace_id", s.TraceID().String()), 120 | zap.String("span_id", s.SpanID().String()), 121 | ) 122 | useLogger(nlg) 123 | } 124 | }) 125 | b.Run("Equal", func(b *testing.B) { 126 | b.ReportAllocs() 127 | tracer := newTestTracer() 128 | 129 | ctx, span := tracer.Start(ctx, "test") 130 | defer span.End() 131 | s := trace.SpanContextFromContext(ctx) 132 | traceIDField := zap.String("trace_id", s.TraceID().String()) 133 | spanIDField := zap.String("span_id", s.SpanID().String()) 134 | 135 | b.ResetTimer() 136 | for i := 0; i < b.N; i++ { 137 | if v := trace.SpanContextFromContext(ctx); v.Equal(s) { 138 | nlg := lg.With(traceIDField, spanIDField) 139 | useLogger(nlg) 140 | } else { 141 | panic("?") 142 | } 143 | } 144 | }) 145 | } 146 | -------------------------------------------------------------------------------- /zctx/zctx_test.go: -------------------------------------------------------------------------------- 1 | package zctx 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | tracesdk "go.opentelemetry.io/otel/sdk/trace" 13 | "go.opentelemetry.io/otel/sdk/trace/tracetest" 14 | "go.opentelemetry.io/otel/trace" 15 | "go.uber.org/zap" 16 | "go.uber.org/zap/zapcore" 17 | "go.uber.org/zap/zaptest/observer" 18 | ) 19 | 20 | func newTestTracer() trace.Tracer { 21 | exporter := tracetest.NewInMemoryExporter() 22 | randSource := rand.NewSource(15) 23 | tp := tracesdk.NewTracerProvider( 24 | // Using deterministic random ids. 25 | tracesdk.WithIDGenerator(&randomIDGenerator{ 26 | rand: rand.New(randSource), 27 | }), 28 | tracesdk.WithBatcher(exporter, 29 | tracesdk.WithBatchTimeout(0), // instant 30 | ), 31 | ) 32 | return tp.Tracer("test") 33 | } 34 | 35 | func assertEmpty(t testing.TB, logs *observer.ObservedLogs) { 36 | t.Helper() 37 | assert.Equal(t, 0, logs.Len(), "Expected empty ObservedLogs to have zero length.") 38 | assert.Equal(t, []observer.LoggedEntry{}, logs.All(), "Unexpected LoggedEntries in empty ObservedLogs.") 39 | } 40 | 41 | func assertEntries(t testing.TB, logs *observer.ObservedLogs, want ...observer.LoggedEntry) { 42 | t.Helper() 43 | 44 | all := logs.TakeAll() 45 | for i := range all { 46 | all[i].Time = time.Time{} 47 | } 48 | 49 | assert.Equal(t, len(want), len(all), "Unexpected observed logs Len.") 50 | 51 | for i := 0; i < len(want); i++ { 52 | b, a := all[i], want[i] 53 | assert.Equalf(t, a.Message, b.Message, "[%d]: Unexpected message.", i) 54 | assert.Equalf(t, a.Level, b.Level, "[%d]: Unexpected level.", i) 55 | 56 | if assert.Equalf(t, len(a.Context), len(b.Context), "[%d]: Unexpected context length.", i) { 57 | expectedFields := make(map[string]zap.Field, len(a.Context)) 58 | haveFields := make(map[string]zap.Field, len(b.Context)) 59 | for j := 0; j < len(a.Context); j++ { 60 | expectedFields[a.Context[j].Key] = a.Context[j] 61 | } 62 | for j := 0; j < len(b.Context); j++ { 63 | haveFields[b.Context[j].Key] = b.Context[j] 64 | } 65 | for k, v := range expectedFields { 66 | if _, ok := haveFields[k]; !ok { 67 | t.Errorf("[%d]: Missing field %q.", i, k) 68 | continue 69 | } 70 | af, hf := v, haveFields[k] 71 | assert.Equalf(t, af.Key, hf.Key, "[%d][%s]: Unexpected context key.", i, k) 72 | if aCtx, aOk := af.Interface.(SpanCompare); aOk { 73 | hCtx, hOk := hf.Interface.(context.Context) 74 | assert.Truef(t, hOk, "[%d][%s]: Unexpected context value.", i, k) 75 | hS := trace.SpanContextFromContext(hCtx) 76 | assert.Truef(t, aCtx.Equal(hS), "[%d][%s]: Unexpected span context. (%s != %s-%s)", 77 | i, k, aCtx, hS.TraceID(), hS.SpanID(), 78 | ) 79 | } else { 80 | assert.Equalf(t, af.Type, hf.Type, "[%d][%s]: Unexpected context type.", i, k) 81 | assert.Equalf(t, af.Interface, hf.Interface, "[%d][%s]: Unexpected context value.", i, k) 82 | } 83 | assert.Equalf(t, af.String, hf.String, "[%d][%s]: Unexpected context value.", i, k) 84 | } 85 | } 86 | } 87 | } 88 | 89 | type randomIDGenerator struct { 90 | sync.Mutex 91 | rand *rand.Rand 92 | } 93 | 94 | // NewSpanID returns a non-zero span ID from a randomly-chosen sequence. 95 | func (gen *randomIDGenerator) NewSpanID(_ context.Context, _ trace.TraceID) (sid trace.SpanID) { 96 | gen.Lock() 97 | defer gen.Unlock() 98 | gen.rand.Read(sid[:]) 99 | return sid 100 | } 101 | 102 | // NewIDs returns a non-zero trace ID and a non-zero span ID from a 103 | // randomly-chosen sequence. 104 | func (gen *randomIDGenerator) NewIDs(_ context.Context) (tid trace.TraceID, sid trace.SpanID) { 105 | gen.Lock() 106 | defer gen.Unlock() 107 | gen.rand.Read(tid[:]) 108 | gen.rand.Read(sid[:]) 109 | return tid, sid 110 | } 111 | 112 | func do(ctx context.Context, tracer trace.Tracer, depth int) { 113 | ctx, span := tracer.Start(ctx, fmt.Sprintf("do(%d)", depth)) 114 | From(ctx).Info("do", zap.Int("depth", depth)) 115 | if depth > 0 { 116 | do(ctx, tracer, depth-1) 117 | } 118 | defer span.End() 119 | } 120 | 121 | func TestFrom(t *testing.T) { 122 | obs, logs := observer.New(zap.DebugLevel) 123 | assertEmpty(t, logs) 124 | 125 | assert.NoError(t, obs.Sync(), "Unexpected failure in no-op Sync") 126 | 127 | lg := zap.New(obs).With(zap.Int("i", 1)) 128 | lg.Info("foo") 129 | 130 | assertEntries(t, logs, observer.LoggedEntry{ 131 | Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "foo"}, 132 | Context: []zapcore.Field{zap.Int("i", 1)}, 133 | }) 134 | 135 | ctx := Base(context.Background(), lg) 136 | From(ctx).Info("baz") 137 | assertEntries(t, logs, observer.LoggedEntry{ 138 | Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "baz"}, 139 | Context: []zapcore.Field{zap.Int("i", 1)}, 140 | }) 141 | 142 | ctx = With(ctx, zap.Int("j", 2)) 143 | From(ctx).Info("baz") 144 | assertEntries(t, logs, observer.LoggedEntry{ 145 | Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "baz"}, 146 | Context: []zapcore.Field{zap.Int("i", 1), zap.Int("j", 2)}, 147 | }) 148 | 149 | tracer := newTestTracer() 150 | do(ctx, tracer, 3) 151 | want := []observer.LoggedEntry{ 152 | { 153 | Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, 154 | Context: []zapcore.Field{ 155 | zap.Int("depth", 3), 156 | zap.String("trace_id", "47058b76ab7d2a10a2ef6534312d205a"), 157 | zap.String("span_id", "aa1a08609e5aacf2"), 158 | zap.Int("i", 1), zap.Int("j", 2), 159 | }, 160 | }, 161 | { 162 | Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, 163 | Context: []zapcore.Field{ 164 | zap.Int("depth", 2), 165 | zap.String("trace_id", "47058b76ab7d2a10a2ef6534312d205a"), 166 | zap.String("span_id", "572a3c21b660fc50"), 167 | zap.Int("i", 1), zap.Int("j", 2), 168 | }, 169 | }, 170 | { 171 | Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, 172 | Context: []zapcore.Field{ 173 | zap.Int("depth", 1), 174 | zap.String("trace_id", "47058b76ab7d2a10a2ef6534312d205a"), 175 | zap.String("span_id", "07b95cb1be0ea6cd"), 176 | zap.Int("i", 1), zap.Int("j", 2), 177 | }, 178 | }, 179 | { 180 | Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, 181 | Context: []zapcore.Field{ 182 | zap.Int("depth", 4), 183 | zap.String("trace_id", "47058b76ab7d2a10a2ef6534312d205a"), 184 | zap.String("span_id", "6f539157d0433b08"), 185 | zap.Int("i", 1), zap.Int("j", 2), 186 | }, 187 | }, 188 | } 189 | assertEntries(t, logs, want...) 190 | } 191 | 192 | type SpanCompare struct { 193 | TraceID string 194 | SpanID string 195 | } 196 | 197 | type SpanComparator interface { 198 | Equal(trace.SpanContext) bool 199 | } 200 | 201 | func newSpanComparator(traceID, spanID string) zap.Field { 202 | return zap.Any("ctx", SpanComparator(SpanCompare{ 203 | TraceID: traceID, 204 | SpanID: spanID, 205 | })) 206 | } 207 | 208 | func (c SpanCompare) Equal(sc trace.SpanContext) bool { 209 | if c.TraceID != sc.TraceID().String() { 210 | return false 211 | } 212 | if c.SpanID != sc.SpanID().String() { 213 | return false 214 | } 215 | return true 216 | } 217 | 218 | func TestOpenTelemetyZap(t *testing.T) { 219 | obs, logs := observer.New(zap.DebugLevel) 220 | assertEmpty(t, logs) 221 | 222 | assert.NoError(t, obs.Sync(), "Unexpected failure in no-op Sync") 223 | 224 | lg := zap.New(obs).With(zap.Int("i", 1)) 225 | 226 | ctx := Base(context.Background(), lg) 227 | ctx = WithOpenTelemetryZap(ctx) 228 | ctx = With(ctx, zap.Int("j", 2)) 229 | 230 | tracer := newTestTracer() 231 | do(ctx, tracer, 3) 232 | want := []observer.LoggedEntry{ 233 | { 234 | Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, 235 | Context: []zapcore.Field{ 236 | zap.Int("depth", 3), 237 | zap.Int("i", 1), zap.Int("j", 2), 238 | newSpanComparator("47058b76ab7d2a10a2ef6534312d205a", "aa1a08609e5aacf2"), 239 | }, 240 | }, 241 | { 242 | Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, 243 | Context: []zapcore.Field{ 244 | zap.Int("depth", 2), 245 | zap.Int("i", 1), zap.Int("j", 2), 246 | newSpanComparator("47058b76ab7d2a10a2ef6534312d205a", "572a3c21b660fc50"), 247 | }, 248 | }, 249 | { 250 | Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, 251 | Context: []zapcore.Field{ 252 | zap.Int("depth", 1), 253 | zap.Int("i", 1), zap.Int("j", 2), 254 | newSpanComparator("47058b76ab7d2a10a2ef6534312d205a", "07b95cb1be0ea6cd"), 255 | }, 256 | }, 257 | { 258 | Entry: zapcore.Entry{Level: zap.InfoLevel, Message: "do"}, 259 | Context: []zapcore.Field{ 260 | zap.Int("depth", 4), 261 | zap.Int("i", 1), zap.Int("j", 2), 262 | newSpanComparator("47058b76ab7d2a10a2ef6534312d205a", "6f539157d0433b08"), 263 | }, 264 | }, 265 | } 266 | assertEntries(t, logs, want...) 267 | } 268 | --------------------------------------------------------------------------------