├── .codecov.yaml ├── .github ├── dependabot.yml └── workflows │ └── x.yml ├── .gitignore ├── .ogen.yml ├── LICENSE ├── Makefile ├── README.md ├── _oas └── openapi.yml ├── client.Dockerfile ├── cmd ├── api-client │ └── main.go └── api-server │ └── main.go ├── docker-compose.yml ├── gen.go ├── go.coverage.sh ├── go.mod ├── go.sum ├── go.test.sh ├── grafana ├── dashboards.yml ├── dashboards │ └── demo.json └── datasources.yml ├── internal ├── api │ └── handler.go ├── httpmiddleware │ ├── httpmiddleware.go │ ├── httpmiddleware_test.go │ ├── ogen.go │ └── provider.go └── oas │ ├── oas_cfg_gen.go │ ├── oas_client_gen.go │ ├── oas_handlers_gen.go │ ├── oas_interfaces_gen.go │ ├── oas_json_gen.go │ ├── oas_labeler_gen.go │ ├── oas_middleware_gen.go │ ├── oas_operations_gen.go │ ├── oas_parameters_gen.go │ ├── oas_request_decoders_gen.go │ ├── oas_request_encoders_gen.go │ ├── oas_response_decoders_gen.go │ ├── oas_response_encoders_gen.go │ ├── oas_router_gen.go │ ├── oas_schemas_gen.go │ ├── oas_server_gen.go │ ├── oas_unimplemented_gen.go │ └── oas_validators_gen.go ├── server.Dockerfile └── tools.go /.codecov.yaml: -------------------------------------------------------------------------------- 1 | ignore: 2 | # Ignore generated files. 3 | - "**/oas_*_gen.go" 4 | # Ignore commands (examples or internal utilities). 5 | - "cmd/*/*.go" 6 | - "**/cmd/*/*.go" 7 | 8 | coverage: 9 | status: 10 | project: false 11 | patch: false 12 | -------------------------------------------------------------------------------- /.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/workflows/x.yml: -------------------------------------------------------------------------------- 1 | name: x 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | # See https://github.com/go-faster/x 10 | jobs: 11 | test: 12 | uses: go-faster/x/.github/workflows/test.yml@main 13 | cover: 14 | uses: go-faster/x/.github/workflows/cover.yml@main 15 | lint: 16 | uses: go-faster/x/.github/workflows/lint.yml@main 17 | commit: 18 | uses: go-faster/x/.github/workflows/commit.yml@main 19 | codeql: 20 | uses: go-faster/x/.github/workflows/codeql.yml@main -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Go template 2 | # If you prefer the allow list template instead of the deny list, see community template: 3 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 4 | # 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | # Go workspace file 22 | go.work 23 | 24 | -------------------------------------------------------------------------------- /.ogen.yml: -------------------------------------------------------------------------------- 1 | generator: 2 | features: 3 | enable: 4 | - "ogen/otel" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @./go.test.sh 3 | .PHONY: test 4 | 5 | coverage: 6 | @./go.coverage.sh 7 | .PHONY: coverage 8 | 9 | generate: 10 | go generate ./... 11 | .PHONY: generate 12 | 13 | check_generated: generate 14 | git diff --exit-code 15 | .PHONY: check_generated 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | Example for generated server and client, instrumented with OpenTelemetry. 4 | 5 | * Force `go.mod` to have ogen in [tools.go](./tools.go) 6 | * Generate code by [gen.go](./gen.go) 7 | * OpenTelemetry setup boilerplate in [go-faster/sdk](https://pkg.go.dev/github.com/go-faster/sdk/app) 8 | * Advanced `net/http` instrumentation in [internal/httpmiddleware](./internal/httpmiddleware) 9 | 10 | ```bash 11 | docker compose up 12 | ``` 13 | 14 | You can open Grafana dashboard on http://localhost:3000 to observe telemetry. 15 | For example, you can see client traces in [TraceQL explore][traces]. 16 | 17 | [traces]: http://localhost:3000/explore?orgId=1&left=%7B%22datasource%22:%22tempo-oteldb%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22tempo-oteldb%22%7D,%22queryType%22:%22nativeSearch%22,%22limit%22:20,%22serviceName%22:%22client%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D 18 | -------------------------------------------------------------------------------- /_oas/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | servers: 3 | - url: /v3 4 | info: 5 | version: 1.0.0 6 | title: Pet store schema 7 | tags: 8 | - name: pet 9 | description: Everything about your Pets 10 | paths: 11 | /pet: 12 | post: 13 | tags: 14 | - pet 15 | summary: Add a new pet to the store 16 | description: Add a new pet to the store 17 | operationId: addPet 18 | responses: 19 | '200': 20 | description: Successful operation 21 | content: 22 | application/json: 23 | schema: 24 | $ref: '#/components/schemas/Pet' 25 | requestBody: 26 | description: Create a new pet in the store 27 | required: true 28 | content: 29 | application/json: 30 | schema: 31 | $ref: '#/components/schemas/Pet' 32 | '/pet/{petId}': 33 | get: 34 | tags: 35 | - pet 36 | summary: Find pet by ID 37 | description: Returns a single pet 38 | operationId: getPetById 39 | parameters: 40 | - name: petId 41 | in: path 42 | description: ID of pet to return 43 | required: true 44 | schema: 45 | type: integer 46 | format: int64 47 | responses: 48 | '200': 49 | description: successful operation 50 | content: 51 | application/json: 52 | schema: 53 | $ref: '#/components/schemas/Pet' 54 | '404': 55 | description: Pet not found 56 | post: 57 | tags: 58 | - pet 59 | summary: Updates a pet in the store 60 | description: '' 61 | operationId: updatePet 62 | parameters: 63 | - name: petId 64 | in: path 65 | description: ID of pet that needs to be updated 66 | required: true 67 | schema: 68 | type: integer 69 | format: int64 70 | - name: name 71 | in: query 72 | description: Name of pet that needs to be updated 73 | schema: 74 | type: string 75 | - name: status 76 | in: query 77 | description: Status of pet that needs to be updated 78 | schema: 79 | $ref: '#/components/schemas/PetStatus' 80 | responses: 81 | '200': 82 | description: successful operation 83 | delete: 84 | tags: 85 | - pet 86 | summary: Deletes a pet 87 | description: '' 88 | operationId: deletePet 89 | parameters: 90 | - name: petId 91 | in: path 92 | description: Pet id to delete 93 | required: true 94 | schema: 95 | type: integer 96 | format: int64 97 | responses: 98 | '200': 99 | description: successful operation 100 | components: 101 | schemas: 102 | PetStatus: 103 | type: string 104 | description: pet status in the store 105 | enum: 106 | - available 107 | - pending 108 | - sold 109 | Pet: 110 | required: 111 | - name 112 | properties: 113 | id: 114 | type: integer 115 | format: int64 116 | example: 10 117 | name: 118 | type: string 119 | example: doggie 120 | photoUrls: 121 | type: array 122 | items: 123 | type: string 124 | status: 125 | $ref: '#/components/schemas/PetStatus' 126 | type: object 127 | requestBodies: 128 | Pet: 129 | content: 130 | application/json: 131 | schema: 132 | $ref: '#/components/schemas/Pet' 133 | description: Pet object that needs to be added to the store 134 | -------------------------------------------------------------------------------- /client.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest as builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | 8 | COPY . ./ 9 | RUN CGO_ENABLED=0 go build -o /app/api-client ./cmd/api-client 10 | 11 | FROM alpine:latest 12 | RUN apk --no-cache add ca-certificates 13 | 14 | WORKDIR /app 15 | COPY --from=builder /app/api-client /api-client 16 | 17 | ENTRYPOINT ["/api-client"] 18 | -------------------------------------------------------------------------------- /cmd/api-client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/go-faster/errors" 10 | "github.com/go-faster/sdk/app" 11 | "github.com/go-faster/sdk/zctx" 12 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 13 | "go.uber.org/zap" 14 | 15 | "example/internal/api" 16 | "example/internal/oas" 17 | ) 18 | 19 | func run(ctx context.Context, lg *zap.Logger, m *app.Telemetry) error { 20 | var arg struct { 21 | BaseURL string 22 | ID int64 23 | } 24 | flag.StringVar(&arg.BaseURL, "url", "http://server:8080", "target server url") 25 | flag.Int64Var(&arg.ID, "id", 1337, "pet id to request") 26 | flag.Parse() 27 | 28 | // For route finding. 29 | oasServer, err := oas.NewServer(api.Handler{}) 30 | if err != nil { 31 | return errors.Wrap(err, "server init") 32 | } 33 | 34 | httpClient := &http.Client{ 35 | Transport: otelhttp.NewTransport(http.DefaultTransport, 36 | otelhttp.WithTracerProvider(m.TracerProvider()), 37 | otelhttp.WithMeterProvider(m.MeterProvider()), 38 | otelhttp.WithPropagators(m.TextMapPropagator()), 39 | otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string { 40 | route, ok := oasServer.FindPath(r.Method, r.URL) 41 | if !ok { 42 | return operation 43 | } 44 | return route.OperationID() 45 | }), 46 | ), 47 | } 48 | client, err := oas.NewClient(arg.BaseURL, 49 | oas.WithClient(httpClient), 50 | oas.WithMeterProvider(m.MeterProvider()), 51 | oas.WithTracerProvider(m.TracerProvider()), 52 | ) 53 | if err != nil { 54 | return errors.Wrap(err, "client") 55 | } 56 | 57 | tracer := m.TracerProvider().Tracer("example") 58 | fetchPet := func(ctx context.Context) error { 59 | ctx, span := tracer.Start(ctx, "tick") 60 | defer span.End() 61 | res, err := client.GetPetById(ctx, oas.GetPetByIdParams{ 62 | PetId: arg.ID, 63 | }) 64 | if err != nil { 65 | return errors.Wrap(err, "get pet") 66 | } 67 | zctx.From(ctx).Info("Got pet", zap.Any("pet", res)) 68 | return nil 69 | } 70 | tick := func() { 71 | if err := fetchPet(ctx); err != nil { 72 | zctx.From(ctx).Error("Failed to fetch pet", zap.Error(err)) 73 | } 74 | } 75 | tick() 76 | 77 | ticker := time.NewTicker(time.Second) 78 | defer ticker.Stop() 79 | for { 80 | select { 81 | case <-ctx.Done(): 82 | return nil 83 | case <-ticker.C: 84 | tick() 85 | } 86 | } 87 | } 88 | 89 | func main() { 90 | app.Run(run) 91 | } 92 | -------------------------------------------------------------------------------- /cmd/api-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/go-faster/errors" 10 | "github.com/go-faster/sdk/app" 11 | "github.com/go-faster/sdk/zctx" 12 | "go.uber.org/zap" 13 | "golang.org/x/sync/errgroup" 14 | 15 | "example/internal/api" 16 | "example/internal/httpmiddleware" 17 | "example/internal/oas" 18 | ) 19 | 20 | const shutdownTimeout = 15 * time.Second 21 | 22 | func main() { 23 | app.Run(func(ctx context.Context, lg *zap.Logger, m *app.Telemetry) error { 24 | var arg struct { 25 | Addr string 26 | } 27 | flag.StringVar(&arg.Addr, "addr", "0.0.0.0:8080", "listen address") 28 | flag.Parse() 29 | 30 | lg.Info("Initializing", 31 | zap.String("http.addr", arg.Addr), 32 | ) 33 | oasServer, err := oas.NewServer(api.Handler{}, 34 | oas.WithTracerProvider(m.TracerProvider()), 35 | oas.WithMeterProvider(m.MeterProvider()), 36 | ) 37 | if err != nil { 38 | return errors.Wrap(err, "server init") 39 | } 40 | 41 | // Using OpenTelemetry instrumentation for HTTP server. 42 | routeFinder := httpmiddleware.MakeRouteFinder(oasServer) 43 | httpServer := http.Server{ 44 | ReadHeaderTimeout: time.Second, 45 | Addr: arg.Addr, 46 | Handler: httpmiddleware.Wrap(oasServer, 47 | httpmiddleware.InjectLogger(zctx.From(ctx)), 48 | httpmiddleware.Instrument("api", routeFinder, m), 49 | httpmiddleware.LogRequests(routeFinder), 50 | httpmiddleware.Labeler(routeFinder), 51 | ), 52 | } 53 | g, ctx := errgroup.WithContext(ctx) 54 | g.Go(func() error { 55 | // Wait until g ctx canceled, then try to shut down server. 56 | <-ctx.Done() 57 | 58 | lg.Info("Shutting down", zap.Duration("timeout", shutdownTimeout)) 59 | 60 | shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) 61 | defer cancel() 62 | return httpServer.Shutdown(shutdownCtx) 63 | }) 64 | g.Go(func() error { 65 | defer lg.Info("Server stopped") 66 | if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 67 | return errors.Wrap(err, "http") 68 | } 69 | return nil 70 | }) 71 | 72 | return g.Wait() 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | client: 5 | restart: always 6 | build: 7 | context: . 8 | dockerfile: client.Dockerfile 9 | args: 10 | BUILDKIT_INLINE_CACHE: 0 11 | environment: 12 | - OTEL_LOG_LEVEL=debug 13 | - OTEL_METRICS_EXPORTER=otlp 14 | - OTEL_LOGS_EXPORTER=otlp 15 | - OTEL_TRACES_EXPORTER=otlp 16 | - OTEL_EXPORTER_OTLP_INSECURE=true 17 | - OTEL_EXPORTER_OTLP_PROTOCOL=grpc 18 | - OTEL_EXPORTER_OTLP_ENDPOINT=http://oteldb:4317 19 | - OTEL_RESOURCE_ATTRIBUTES=service.name=client 20 | 21 | server: 22 | restart: always 23 | command: ["server"] 24 | build: 25 | context: . 26 | dockerfile: server.Dockerfile 27 | args: 28 | BUILDKIT_INLINE_CACHE: 0 29 | environment: 30 | - OTEL_LOG_LEVEL=debug 31 | - OTEL_METRICS_EXPORTER=otlp 32 | - OTEL_LOGS_EXPORTER=otlp 33 | - OTEL_TRACES_EXPORTER=otlp 34 | - OTEL_EXPORTER_OTLP_PROTOCOL=grpc 35 | - OTEL_EXPORTER_OTLP_INSECURE=true 36 | - OTEL_EXPORTER_OTLP_ENDPOINT=http://oteldb:4317 37 | - OTEL_RESOURCE_ATTRIBUTES=service.name=server 38 | 39 | # Observability stack 40 | clickhouse: 41 | image: clickhouse/clickhouse-server:23.10 42 | volumes: 43 | - ./build/clickhouse.xml:/etc/clickhouse-server/config.d/monitoring.xml 44 | healthcheck: 45 | test: ['CMD', 'wget', '--spider', '-q', '127.0.0.1:8123/ping'] 46 | interval: 1s 47 | timeout: 1s 48 | retries: 30 49 | oteldb: 50 | image: ghcr.io/go-faster/oteldb:0.5.0 51 | environment: 52 | - OTELDB_STORAGE=ch 53 | - CH_DSN=clickhouse://clickhouse:9000 54 | - OTEL_LOG_LEVEL=info 55 | - OTEL_TRACES_EXPORTER=none 56 | - OTEL_METRICS_EXPORTER=none 57 | - OTEL_LOGS_EXPORTER=stdout 58 | - OTEL_RESOURCE_ATTRIBUTES=service.name=oteldb 59 | depends_on: [clickhouse] 60 | grafana: 61 | image: "grafana/grafana:10.0.0" 62 | environment: 63 | - GF_AUTH_ANONYMOUS_ENABLED=true 64 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 65 | - GF_AUTH_DISABLE_LOGIN_FORM=true 66 | - GF_ALERTING_ENABLED=false 67 | - GF_USERS_ALLOW_SIGN_UP=false 68 | - GF_LOG_LEVEL=warn 69 | - GF_INSTALL_PLUGINS=grafana-clickhouse-datasource 70 | volumes: 71 | - ./grafana/datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml 72 | - ./grafana/dashboards.yml:/etc/grafana/provisioning/dashboards/default.yml 73 | - ./grafana/dashboards:/etc/grafana/dashboards:ro 74 | ports: 75 | - "3000:3000" 76 | -------------------------------------------------------------------------------- /gen.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | //go:generate go run github.com/ogen-go/ogen/cmd/ogen --clean --package oas --target internal/oas _oas/openapi.yml 4 | -------------------------------------------------------------------------------- /go.coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | go test -v -coverpkg=./... -coverprofile=profile.out ./... 6 | go tool cover -func profile.out 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/go-faster/errors v0.7.1 7 | github.com/go-faster/jx v1.1.0 8 | github.com/go-faster/sdk v0.27.0 9 | github.com/ogen-go/ogen v1.13.0 10 | github.com/stretchr/testify v1.10.0 11 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 12 | go.opentelemetry.io/otel v1.36.0 13 | go.opentelemetry.io/otel/metric v1.36.0 14 | go.opentelemetry.io/otel/sdk v1.36.0 15 | go.opentelemetry.io/otel/trace v1.36.0 16 | go.uber.org/multierr v1.11.0 17 | go.uber.org/zap v1.27.0 18 | golang.org/x/sync v0.14.0 19 | ) 20 | 21 | require ( 22 | github.com/KimMachineGun/automemlimit v0.7.0 // indirect 23 | github.com/beorn7/perks v1.0.1 // indirect 24 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 25 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 26 | github.com/davecgh/go-spew v1.1.1 // indirect 27 | github.com/dlclark/regexp2 v1.11.5 // indirect 28 | github.com/fatih/color v1.18.0 // indirect 29 | github.com/felixge/httpsnoop v1.0.4 // indirect 30 | github.com/ghodss/yaml v1.0.0 // indirect 31 | github.com/go-faster/yaml v0.4.6 // indirect 32 | github.com/go-logr/logr v1.4.2 // indirect 33 | github.com/go-logr/stdr v1.2.2 // indirect 34 | github.com/go-logr/zapr v1.3.0 // indirect 35 | github.com/google/uuid v1.6.0 // indirect 36 | github.com/grafana/otel-profiling-go v0.5.1 // indirect 37 | github.com/grafana/pyroscope-go v1.2.0 // indirect 38 | github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect 39 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect 40 | github.com/klauspost/compress v1.18.0 // indirect 41 | github.com/mattn/go-colorable v0.1.13 // indirect 42 | github.com/mattn/go-isatty v0.0.20 // indirect 43 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 44 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 45 | github.com/pmezard/go-difflib v1.0.0 // indirect 46 | github.com/prometheus/client_golang v1.20.5 // indirect 47 | github.com/prometheus/client_model v0.6.1 // indirect 48 | github.com/prometheus/common v0.61.0 // indirect 49 | github.com/prometheus/procfs v0.15.1 // indirect 50 | github.com/samber/lo v1.47.0 // indirect 51 | github.com/samber/slog-common v0.18.1 // indirect 52 | github.com/samber/slog-zap/v2 v2.6.2 // indirect 53 | github.com/segmentio/asm v1.2.0 // indirect 54 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 55 | go.opentelemetry.io/contrib/bridges/otelzap v0.9.0 // indirect 56 | go.opentelemetry.io/contrib/instrumentation/runtime v0.59.0 // indirect 57 | go.opentelemetry.io/contrib/propagators/autoprop v0.59.0 // indirect 58 | go.opentelemetry.io/contrib/propagators/aws v1.34.0 // indirect 59 | go.opentelemetry.io/contrib/propagators/b3 v1.34.0 // indirect 60 | go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect 61 | go.opentelemetry.io/contrib/propagators/ot v1.34.0 // indirect 62 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0 // indirect 63 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 // indirect 64 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 // indirect 65 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0 // indirect 66 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect 67 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect 68 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect 69 | go.opentelemetry.io/otel/exporters/prometheus v0.56.0 // indirect 70 | go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 // indirect 71 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0 // indirect 72 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 // indirect 73 | go.opentelemetry.io/otel/log v0.10.0 // indirect 74 | go.opentelemetry.io/otel/sdk/log v0.10.0 // indirect 75 | go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect 76 | go.opentelemetry.io/proto/otlp v1.5.0 // indirect 77 | go.uber.org/automaxprocs v1.6.0 // indirect 78 | golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect 79 | golang.org/x/mod v0.24.0 // indirect 80 | golang.org/x/net v0.40.0 // indirect 81 | golang.org/x/sys v0.33.0 // indirect 82 | golang.org/x/text v0.25.0 // indirect 83 | golang.org/x/tools v0.33.0 // indirect 84 | google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect 85 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect 86 | google.golang.org/grpc v1.70.0 // indirect 87 | google.golang.org/protobuf v1.36.4 // indirect 88 | gopkg.in/yaml.v2 v2.4.0 // indirect 89 | gopkg.in/yaml.v3 v3.0.1 // indirect 90 | ) 91 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/KimMachineGun/automemlimit v0.7.0 h1:7G06p/dMSf7G8E6oq+f2uOPuVncFyIlDI/pBWK49u88= 2 | github.com/KimMachineGun/automemlimit v0.7.0/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/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 6 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 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/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= 13 | github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 14 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 15 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 16 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 17 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 18 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 19 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 20 | github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= 21 | github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= 22 | github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg= 23 | github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg= 24 | github.com/go-faster/sdk v0.27.0 h1:QAeM1J4jinfYft8yqUfOerPN5Kpcfs4DZZTXDLu1g/M= 25 | github.com/go-faster/sdk v0.27.0/go.mod h1:KTBYqEvTRRu5p321ZH0FIavynljDdyBbLeAUOU9nEKE= 26 | github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I= 27 | github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk= 28 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 29 | github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 30 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 31 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 32 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 33 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 34 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 35 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 36 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 37 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 38 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 39 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 40 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 41 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 42 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 43 | github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= 44 | github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= 45 | github.com/grafana/pyroscope-go v1.2.0 h1:aILLKjTj8CS8f/24OPMGPewQSYlhmdQMBmol1d3KGj8= 46 | github.com/grafana/pyroscope-go v1.2.0/go.mod h1:2GHr28Nr05bg2pElS+dDsc98f3JTUh2f6Fz1hWXrqwk= 47 | github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= 48 | github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= 49 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= 50 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= 51 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 52 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 53 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 54 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 55 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 56 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 57 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 58 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 59 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 60 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 61 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 62 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 63 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 64 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 65 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 66 | github.com/ogen-go/ogen v1.13.0 h1:RI3jAMZvn6fIlFCZR8g9KqTmpGRxBMmsax1qcjhcD38= 67 | github.com/ogen-go/ogen v1.13.0/go.mod h1:SNGTKeDIFhILb0+22f+gkT1FaeYmFgKrNmzUXMsnDro= 68 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= 69 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= 70 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 71 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 72 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 73 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 74 | github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= 75 | github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 76 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 77 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 78 | github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= 79 | github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= 80 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 81 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 82 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 83 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 84 | github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= 85 | github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= 86 | github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ= 87 | github.com/samber/slog-common v0.18.1/go.mod h1:QNZiNGKakvrfbJ2YglQXLCZauzkI9xZBjOhWFKS3IKk= 88 | github.com/samber/slog-zap/v2 v2.6.2 h1:IPHgVQjBfEwqu7fBxSxvvl+/E4b7TqAu/eispdQdv9M= 89 | github.com/samber/slog-zap/v2 v2.6.2/go.mod h1:bMOphuaRcThr+2X7vE4kFaqyr1lqGkc9Js95n9X6xaU= 90 | github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 91 | github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 92 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 93 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 94 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 95 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 96 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 97 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 98 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 99 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 100 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 101 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 102 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 103 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 104 | go.opentelemetry.io/contrib/bridges/otelzap v0.9.0 h1:f+xpAfhQTjR8beiSMe1bnT/25PkeyWmOcI+SjXWguNw= 105 | go.opentelemetry.io/contrib/bridges/otelzap v0.9.0/go.mod h1:T1Z1jyS5FttgQoF6UcGhnM+gF9wU32B4lHO69nXw4FE= 106 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= 107 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= 108 | go.opentelemetry.io/contrib/instrumentation/runtime v0.59.0 h1:rfi2MMujBc4yowE0iHckZX4o4jg6SA67EnFVL8ldVvU= 109 | go.opentelemetry.io/contrib/instrumentation/runtime v0.59.0/go.mod h1:IO/gfPEcQYpOpPxn1OXFp1DvRY0viP8ONMedXLjjHIU= 110 | go.opentelemetry.io/contrib/propagators/autoprop v0.59.0 h1:bgG6F0HBLngIG79m8VYMdgh3adfcjCgLbsO8StsovQk= 111 | go.opentelemetry.io/contrib/propagators/autoprop v0.59.0/go.mod h1:JV4DSHIsqQoFVaeE6xDef6xdI2I/IOOsWnXWCzQ0EXQ= 112 | go.opentelemetry.io/contrib/propagators/aws v1.34.0 h1:pv/Yi44N2BM1Kyl6wxO6bTiwcxUA7Deog3Rc7NO9ITE= 113 | go.opentelemetry.io/contrib/propagators/aws v1.34.0/go.mod h1:1aF3HFtAyIi+B2xJHOdKQcNz+bcDS+JLAZjsohcW1P4= 114 | go.opentelemetry.io/contrib/propagators/b3 v1.34.0 h1:9pQdCEvV/6RWQmag94D6rhU+A4rzUhYBEJ8bpscx5p8= 115 | go.opentelemetry.io/contrib/propagators/b3 v1.34.0/go.mod h1:FwM71WS8i1/mAK4n48t0KU6qUS/OZRBgDrHZv3RlJ+w= 116 | go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= 117 | go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= 118 | go.opentelemetry.io/contrib/propagators/ot v1.34.0 h1:fcA0FMvHmco/mjXbmRoNx9IRs6+UuRVyXhMHzP+pEc8= 119 | go.opentelemetry.io/contrib/propagators/ot v1.34.0/go.mod h1:VvybuUSU0G7m9DLza4YX8KKniYlIphI+mZ6ufkaV1m8= 120 | go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= 121 | go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= 122 | go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= 123 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0 h1:5dTKu4I5Dn4P2hxyW3l3jTaZx9ACgg0ECos1eAVrheY= 124 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0/go.mod h1:P5HcUI8obLrCCmM3sbVBohZFH34iszk/+CPWuakZWL8= 125 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 h1:q/heq5Zh8xV1+7GoMGJpTxM2Lhq5+bFxB29tshuRuw0= 126 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0/go.mod h1:leO2CSTg0Y+LyvmR7Wm4pUxE8KAmaM2GCVx7O+RATLA= 127 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 h1:ajl4QczuJVA2TU9W9AGw++86Xga/RKt//16z/yxPgdk= 128 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0/go.mod h1:Vn3/rlOJ3ntf/Q3zAI0V5lDnTbHGaUsNUeF6nZmm7pA= 129 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0 h1:opwv08VbCZ8iecIWs+McMdHRcAXzjAeda3uG2kI/hcA= 130 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0/go.mod h1:oOP3ABpW7vFHulLpE8aYtNBodrHhMTrvfxUXGvqm7Ac= 131 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= 132 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= 133 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= 134 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= 135 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= 136 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= 137 | go.opentelemetry.io/otel/exporters/prometheus v0.56.0 h1:GnCIi0QyG0yy2MrJLzVrIM7laaJstj//flf1zEJCG+E= 138 | go.opentelemetry.io/otel/exporters/prometheus v0.56.0/go.mod h1:JQcVZtbIIPM+7SWBB+T6FK+xunlyidwLp++fN0sUaOk= 139 | go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 h1:GKCEAZLEpEf78cUvudQdTg0aET2ObOZRB2HtXA0qPAI= 140 | go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0/go.mod h1:9/zqSWLCmHT/9Jo6fYeUDRRogOLL60ABLsHWS99lF8s= 141 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0 h1:czJDQwFrMbOr9Kk+BPo1y8WZIIFIK58SA1kykuVeiOU= 142 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0/go.mod h1:lT7bmsxOe58Tq+JIOkTQMCGXdu47oA+VJKLZHbaBKbs= 143 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 h1:jBpDk4HAUsrnVO1FsfCfCOTEc/MkInJmvfCHYLFiT80= 144 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0/go.mod h1:H9LUIM1daaeZaz91vZcfeM0fejXPmgCYE8ZhzqfJuiU= 145 | go.opentelemetry.io/otel/log v0.10.0 h1:1CXmspaRITvFcjA4kyVszuG4HjA61fPDxMb7q3BuyF0= 146 | go.opentelemetry.io/otel/log v0.10.0/go.mod h1:PbVdm9bXKku/gL0oFfUF4wwsQsOPlpo4VEqjvxih+FM= 147 | go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= 148 | go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= 149 | go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= 150 | go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= 151 | go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= 152 | go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= 153 | go.opentelemetry.io/otel/sdk/log v0.10.0 h1:lR4teQGWfeDVGoute6l0Ou+RpFqQ9vaPdrNJlST0bvw= 154 | go.opentelemetry.io/otel/sdk/log v0.10.0/go.mod h1:A+V1UTWREhWAittaQEG4bYm4gAZa6xnvVu+xKrIRkzo= 155 | go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= 156 | go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= 157 | go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= 158 | go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= 159 | go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= 160 | go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= 161 | go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= 162 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 163 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 164 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 165 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 166 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 167 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 168 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 169 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 170 | golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= 171 | golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= 172 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 173 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 174 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 175 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 176 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 177 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 178 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 179 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 180 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 181 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 182 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 183 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 184 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 185 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= 186 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 187 | google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= 188 | google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= 189 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= 190 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= 191 | google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= 192 | google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= 193 | google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= 194 | google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 195 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 196 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 197 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 198 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 199 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 200 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 201 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 202 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 203 | -------------------------------------------------------------------------------- /go.test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # test with -race 6 | go test --timeout 5m -race ./... 7 | -------------------------------------------------------------------------------- /grafana/dashboards.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: Default 5 | folder: Default 6 | type: file 7 | options: 8 | path: /etc/grafana/dashboards 9 | -------------------------------------------------------------------------------- /grafana/dashboards/demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "grafana", 8 | "uid": "-- Grafana --" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "type": "dashboard" 15 | } 16 | ] 17 | }, 18 | "editable": true, 19 | "fiscalYearStartMonth": 0, 20 | "graphTooltip": 0, 21 | "links": [], 22 | "liveNow": false, 23 | "panels": [ 24 | { 25 | "datasource": { 26 | "type": "loki", 27 | "uid": "loki-oteldb" 28 | }, 29 | "gridPos": { 30 | "h": 11, 31 | "w": 24, 32 | "x": 0, 33 | "y": 0 34 | }, 35 | "id": 2, 36 | "options": { 37 | "dedupStrategy": "none", 38 | "enableLogDetails": true, 39 | "prettifyLogMessage": false, 40 | "showCommonLabels": false, 41 | "showLabels": false, 42 | "showTime": false, 43 | "sortOrder": "Descending", 44 | "wrapLogMessage": false 45 | }, 46 | "targets": [ 47 | { 48 | "datasource": { 49 | "type": "loki", 50 | "uid": "loki-oteldb" 51 | }, 52 | "editorMode": "builder", 53 | "expr": "{service_name=\"client\"} |= ``", 54 | "queryType": "range", 55 | "refId": "A" 56 | } 57 | ], 58 | "title": "client", 59 | "type": "logs" 60 | }, 61 | { 62 | "datasource": { 63 | "type": "loki", 64 | "uid": "loki-oteldb" 65 | }, 66 | "gridPos": { 67 | "h": 11, 68 | "w": 24, 69 | "x": 0, 70 | "y": 11 71 | }, 72 | "id": 3, 73 | "options": { 74 | "dedupStrategy": "none", 75 | "enableLogDetails": true, 76 | "prettifyLogMessage": false, 77 | "showCommonLabels": false, 78 | "showLabels": false, 79 | "showTime": false, 80 | "sortOrder": "Descending", 81 | "wrapLogMessage": false 82 | }, 83 | "targets": [ 84 | { 85 | "datasource": { 86 | "type": "loki", 87 | "uid": "loki-oteldb" 88 | }, 89 | "editorMode": "builder", 90 | "expr": "{service_name=\"server\"} |= ``", 91 | "queryType": "range", 92 | "refId": "A" 93 | } 94 | ], 95 | "title": "client", 96 | "type": "logs" 97 | }, 98 | { 99 | "datasource": { 100 | "type": "prometheus", 101 | "uid": "prom-oteldb" 102 | }, 103 | "fieldConfig": { 104 | "defaults": { 105 | "color": { 106 | "mode": "palette-classic" 107 | }, 108 | "custom": { 109 | "axisCenteredZero": false, 110 | "axisColorMode": "text", 111 | "axisLabel": "", 112 | "axisPlacement": "auto", 113 | "barAlignment": 0, 114 | "drawStyle": "line", 115 | "fillOpacity": 0, 116 | "gradientMode": "none", 117 | "hideFrom": { 118 | "legend": false, 119 | "tooltip": false, 120 | "viz": false 121 | }, 122 | "lineInterpolation": "linear", 123 | "lineWidth": 1, 124 | "pointSize": 5, 125 | "scaleDistribution": { 126 | "type": "linear" 127 | }, 128 | "showPoints": "auto", 129 | "spanNulls": false, 130 | "stacking": { 131 | "group": "A", 132 | "mode": "none" 133 | }, 134 | "thresholdsStyle": { 135 | "mode": "off" 136 | } 137 | }, 138 | "mappings": [], 139 | "thresholds": { 140 | "mode": "absolute", 141 | "steps": [ 142 | { 143 | "color": "green", 144 | "value": null 145 | }, 146 | { 147 | "color": "red", 148 | "value": 80 149 | } 150 | ] 151 | } 152 | }, 153 | "overrides": [] 154 | }, 155 | "gridPos": { 156 | "h": 10, 157 | "w": 24, 158 | "x": 0, 159 | "y": 22 160 | }, 161 | "id": 1, 162 | "options": { 163 | "legend": { 164 | "calcs": [], 165 | "displayMode": "list", 166 | "placement": "bottom", 167 | "showLegend": true 168 | }, 169 | "tooltip": { 170 | "mode": "single", 171 | "sort": "none" 172 | } 173 | }, 174 | "targets": [ 175 | { 176 | "datasource": { 177 | "type": "prometheus", 178 | "uid": "prom-oteldb" 179 | }, 180 | "editorMode": "code", 181 | "exemplar": true, 182 | "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[$__rate_interval])) by (le))", 183 | "instant": false, 184 | "range": true, 185 | "refId": "A" 186 | } 187 | ], 188 | "title": "Histogram", 189 | "type": "timeseries" 190 | } 191 | ], 192 | "refresh": "", 193 | "schemaVersion": 38, 194 | "style": "dark", 195 | "tags": [], 196 | "templating": { 197 | "list": [] 198 | }, 199 | "time": { 200 | "from": "now-1m", 201 | "to": "now" 202 | }, 203 | "timepicker": {}, 204 | "timezone": "", 205 | "title": "Demo", 206 | "uid": "demo", 207 | "version": 1, 208 | "weekStart": "" 209 | } -------------------------------------------------------------------------------- /grafana/datasources.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: "TraceQL" 5 | type: tempo 6 | access: proxy 7 | orgId: 1 8 | url: http://oteldb:3200 9 | uid: tempo-oteldb 10 | jsonData: 11 | httpMethod: GET 12 | tracesToLogsV2: 13 | datasourceUid: 'loki-oteldb' 14 | spanStartTimeShift: '1h' 15 | spanEndTimeShift: '-1h' 16 | filterByTraceID: true 17 | filterBySpanID: false 18 | tags: 19 | - key: service.name 20 | value: service_name 21 | tracesToMetrics: 22 | datasourceUid: 'prom-oteldb' 23 | spanStartTimeShift: '1h' 24 | spanEndTimeShift: '-1h' 25 | tags: 26 | - key: service.name 27 | value: service_name 28 | nodeGraph: 29 | enabled: true 30 | serviceMap: 31 | datasourceUid: prom-oteldb 32 | lokiSearch: 33 | datasourceUid: 'loki-oteldb' 34 | 35 | - name: "LogQL" 36 | type: loki 37 | access: proxy 38 | orgId: 1 39 | url: http://oteldb:3100 40 | uid: loki-oteldb 41 | jsonData: 42 | serviceMap: 43 | datasourceUid: prom-oteldb 44 | maxLines: 500 45 | derivedFields: 46 | - datasourceUid: tempo-oteldb 47 | matcherRegex: '"trace_id":"([0-9a-f]+)"' 48 | name: trace 49 | url: '$${__value.raw}' 50 | urlDisplayLabel: 'View Trace' 51 | 52 | - name: "PromQL" 53 | type: prometheus 54 | access: proxy 55 | orgId: 1 56 | isDefault: true 57 | url: http://oteldb:9090 58 | uid: prom-oteldb 59 | 60 | jsonData: 61 | prometheusType: Prometheus 62 | disableRecordingRules: false 63 | exemplarTraceIdDestinations: 64 | - datasourceUid: tempo-oteldb 65 | name: trace_id 66 | -------------------------------------------------------------------------------- /internal/api/handler.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-faster/sdk/zctx" 8 | "go.uber.org/zap" 9 | 10 | "example/internal/oas" 11 | ) 12 | 13 | // Compile-time check for Handler. 14 | var _ oas.Handler = (*Handler)(nil) 15 | 16 | type Handler struct { 17 | oas.UnimplementedHandler // automatically implement all methods 18 | } 19 | 20 | func (h Handler) GetPetById(ctx context.Context, params oas.GetPetByIdParams) (oas.GetPetByIdRes, error) { 21 | zctx.From(ctx).Info("GetPetById", zap.Any("params", params)) 22 | return &oas.Pet{ 23 | ID: oas.NewOptInt64(params.PetId), 24 | Name: fmt.Sprintf("Pet %d", params.PetId), 25 | Status: oas.NewOptPetStatus(oas.PetStatusAvailable), 26 | }, nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/httpmiddleware/httpmiddleware.go: -------------------------------------------------------------------------------- 1 | // Package httpmiddleware contains HTTP middlewares. 2 | package httpmiddleware 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/go-faster/sdk/zctx" 8 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 9 | "go.opentelemetry.io/otel/metric" 10 | "go.opentelemetry.io/otel/propagation" 11 | semconv "go.opentelemetry.io/otel/semconv/v1.21.0" 12 | "go.opentelemetry.io/otel/trace" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | // Middleware is a net/http middleware. 17 | type Middleware = func(http.Handler) http.Handler 18 | 19 | // InjectLogger injects logger into request context. 20 | func InjectLogger(lg *zap.Logger) Middleware { 21 | return func(next http.Handler) http.Handler { 22 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 23 | reqCtx := r.Context() 24 | req := r.WithContext(zctx.Base(reqCtx, lg)) 25 | next.ServeHTTP(w, req) 26 | }) 27 | } 28 | } 29 | 30 | func Labeler(find RouteFinder) Middleware { 31 | return func(h http.Handler) http.Handler { 32 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 | route, ok := find(r.Method, r.URL) 34 | if !ok { 35 | h.ServeHTTP(w, r) 36 | return 37 | } 38 | 39 | attr := semconv.HTTPRouteKey.String(route.PathPattern()) 40 | span := trace.SpanFromContext(r.Context()) 41 | span.SetAttributes(attr) 42 | 43 | labeler, _ := otelhttp.LabelerFromContext(r.Context()) 44 | labeler.Add(attr) 45 | 46 | h.ServeHTTP(w, r) 47 | }) 48 | } 49 | } 50 | 51 | // LogRequests logs incoming requests using context logger. 52 | func LogRequests(find RouteFinder) Middleware { 53 | return func(next http.Handler) http.Handler { 54 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 55 | ctx := r.Context() 56 | lg := zctx.From(ctx) 57 | var ( 58 | opName = zap.Skip() 59 | opID = zap.Skip() 60 | ) 61 | if route, ok := find(r.Method, r.URL); ok { 62 | opName = zap.String("operationName", route.Name()) 63 | opID = zap.String("operationId", route.OperationID()) 64 | } 65 | lg.Info("Got request", 66 | zap.String("method", r.Method), 67 | zap.Stringer("url", r.URL), 68 | opID, 69 | opName, 70 | ) 71 | next.ServeHTTP(w, r) 72 | }) 73 | } 74 | } 75 | 76 | // Metrics wraps TracerProvider and MeterProvider. 77 | type Metrics interface { 78 | TracerProvider() trace.TracerProvider 79 | MeterProvider() metric.MeterProvider 80 | TextMapPropagator() propagation.TextMapPropagator 81 | } 82 | 83 | // Instrument setups otelhttp. 84 | func Instrument(serviceName string, find RouteFinder, m Metrics) Middleware { 85 | return func(h http.Handler) http.Handler { 86 | return otelhttp.NewHandler(h, "", 87 | otelhttp.WithPropagators(m.TextMapPropagator()), 88 | otelhttp.WithTracerProvider(m.TracerProvider()), 89 | otelhttp.WithMeterProvider(m.MeterProvider()), 90 | otelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents), 91 | otelhttp.WithServerName(serviceName), 92 | otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string { 93 | op, ok := find(r.Method, r.URL) 94 | if ok { 95 | return serviceName + "." + op.OperationID() 96 | } 97 | return operation 98 | }), 99 | ) 100 | } 101 | } 102 | 103 | // Wrap handler using given middlewares. 104 | func Wrap(h http.Handler, middlewares ...Middleware) http.Handler { 105 | switch len(middlewares) { 106 | case 0: 107 | return h 108 | case 1: 109 | return middlewares[0](h) 110 | default: 111 | for i := len(middlewares) - 1; i >= 0; i-- { 112 | h = middlewares[i](h) 113 | } 114 | return h 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /internal/httpmiddleware/httpmiddleware_test.go: -------------------------------------------------------------------------------- 1 | // Package httpmiddleware contains HTTP middlewares. 2 | package httpmiddleware 3 | 4 | import ( 5 | "context" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "testing" 10 | 11 | "github.com/go-faster/sdk/zctx" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 15 | "go.opentelemetry.io/otel/trace" 16 | "go.uber.org/zap" 17 | "go.uber.org/zap/zapcore" 18 | "go.uber.org/zap/zaptest/observer" 19 | ) 20 | 21 | type testHandler struct{} 22 | 23 | func (*testHandler) ServeHTTP(http.ResponseWriter, *http.Request) {} 24 | 25 | type testMiddleware struct{} 26 | 27 | func (*testMiddleware) ServeHTTP(http.ResponseWriter, *http.Request) {} 28 | 29 | func TestInjectLogger(t *testing.T) { 30 | core, logs := observer.New(zapcore.DebugLevel) 31 | 32 | h := Wrap( 33 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 34 | lg := zctx.From(r.Context()) 35 | lg.Info("Hello") 36 | }), 37 | InjectLogger(zap.New(core)), 38 | ) 39 | h.ServeHTTP(nil, &http.Request{}) 40 | 41 | entries := logs.FilterLevelExact(zapcore.InfoLevel).All() 42 | require.Len(t, entries, 1) 43 | 44 | entry := entries[0] 45 | require.Equal(t, "Hello", entry.Message) 46 | } 47 | 48 | type testOgenServer struct{} 49 | 50 | func (*testOgenServer) FindPath(method string, u *url.URL) (r testOgenRoute, _ bool) { 51 | if method != http.MethodGet || u.Path != "/foo" { 52 | return r, false 53 | } 54 | return r, true 55 | } 56 | 57 | type testOgenRoute struct{} 58 | 59 | func (testOgenRoute) Name() string { return "TestRoute" } 60 | func (testOgenRoute) OperationID() string { return "testRoute" } 61 | func (testOgenRoute) PathPattern() string { return "/foo" } 62 | 63 | func TestLogRequests(t *testing.T) { 64 | core, logs := observer.New(zapcore.DebugLevel) 65 | 66 | h := Wrap(&testHandler{}, 67 | InjectLogger(zap.New(core)), 68 | LogRequests(MakeRouteFinder(&testOgenServer{})), 69 | ) 70 | h.ServeHTTP(nil, &http.Request{ 71 | Method: http.MethodPost, 72 | URL: &url.URL{ 73 | Path: "/unknown_path", 74 | }, 75 | }) 76 | h.ServeHTTP(nil, &http.Request{ 77 | Method: http.MethodGet, 78 | URL: &url.URL{ 79 | Path: "/foo", 80 | }, 81 | }) 82 | 83 | entries := logs.FilterLevelExact(zapcore.InfoLevel).All() 84 | require.Len(t, entries, 2) 85 | 86 | entry := entries[0] 87 | require.Equal(t, "Got request", entry.Message) 88 | fields := entry.ContextMap() 89 | require.Len(t, fields, 2) 90 | require.Equal(t, http.MethodPost, fields["method"]) 91 | require.Equal(t, "/unknown_path", fields["url"]) 92 | 93 | entry = entries[1] 94 | require.Equal(t, "Got request", entry.Message) 95 | fields = entry.ContextMap() 96 | require.Len(t, fields, 4) 97 | require.Equal(t, http.MethodGet, fields["method"]) 98 | require.Equal(t, "/foo", fields["url"]) 99 | require.Equal(t, "TestRoute", fields["operationName"]) 100 | require.Equal(t, "testRoute", fields["operationId"]) 101 | } 102 | 103 | func TestWrap(t *testing.T) { 104 | endpoint := &testHandler{} 105 | 106 | // Check case with zero middlewares. 107 | result := Wrap(endpoint) 108 | require.Equal(t, endpoint, result) 109 | 110 | // Check case with one middleware. 111 | middleware := &testMiddleware{} 112 | result = Wrap(endpoint, func(h http.Handler) http.Handler { 113 | return middleware 114 | }) 115 | require.Equal(t, middleware, result) 116 | 117 | // Ensure order of wrapping. 118 | var ( 119 | calls []int 120 | callMiddleware = func(n int) Middleware { 121 | return func(next http.Handler) http.Handler { 122 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 123 | calls = append(calls, n) 124 | next.ServeHTTP(w, r) 125 | }) 126 | } 127 | } 128 | ) 129 | 130 | result = Wrap(endpoint, 131 | callMiddleware(1), 132 | callMiddleware(2), 133 | callMiddleware(3), 134 | ) 135 | result.ServeHTTP(nil, nil) 136 | require.Equal(t, []int{1, 2, 3}, calls) 137 | } 138 | 139 | func TestInstrumentation(t *testing.T) { 140 | provider := NewProvider() 141 | tracer := provider.Tracer("test") 142 | fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 143 | ctx := r.Context() 144 | t.Logf("Handler(ctx).IsValid(): %v", trace.SpanContextFromContext(ctx).IsValid()) 145 | assert.True(t, trace.SpanContextFromContext(ctx).IsValid()) 146 | _, span := tracer.Start(r.Context(), "Handler") 147 | defer span.End() 148 | w.WriteHeader(http.StatusOK) 149 | }) 150 | h := Wrap(fn, 151 | otelhttp.NewMiddleware("otelhttp.Middleware", 152 | otelhttp.WithTracerProvider(provider), 153 | ), 154 | func(handler http.Handler) http.Handler { 155 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 156 | ctx := r.Context() 157 | sc := trace.SpanContextFromContext(ctx) 158 | t.Logf("after(ctx).IsValid(): %v (%s)", sc.IsValid(), sc.TraceID()) 159 | assert.True(t, sc.IsValid(), "Middleware span should be valid") 160 | 161 | ctx, span := tracer.Start(ctx, "After") 162 | defer span.End() 163 | handler.ServeHTTP(w, r.WithContext(ctx)) 164 | }) 165 | }, 166 | ) 167 | rw := httptest.NewRecorder() 168 | req := &http.Request{ 169 | Method: http.MethodGet, 170 | URL: &url.URL{ 171 | Path: "/foo", 172 | }, 173 | } 174 | h.ServeHTTP(rw, req.WithContext(context.Background())) 175 | require.Equal(t, http.StatusOK, rw.Code) 176 | provider.Flush() 177 | spans := provider.Exporter.GetSpans() 178 | assert.Len(t, spans, 3) 179 | for _, s := range spans { 180 | t.Logf("%s [%s]", s.Name, s.SpanContext.TraceID()) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /internal/httpmiddleware/ogen.go: -------------------------------------------------------------------------------- 1 | package httpmiddleware 2 | 3 | import "net/url" 4 | 5 | // Server is a generic ogen server type. 6 | type Server[R Route] interface { 7 | FindPath(method string, u *url.URL) (r R, _ bool) 8 | } 9 | 10 | // Route is a generic ogen route type. 11 | type Route interface { 12 | Name() string 13 | OperationID() string 14 | PathPattern() string 15 | } 16 | 17 | // RouteFinder finds Route by given URL. 18 | type RouteFinder func(method string, u *url.URL) (Route, bool) 19 | 20 | // MakeRouteFinder creates RouteFinder from given server. 21 | func MakeRouteFinder[R Route, S Server[R]](server S) RouteFinder { 22 | return func(method string, u *url.URL) (Route, bool) { 23 | return server.FindPath(method, u) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/httpmiddleware/provider.go: -------------------------------------------------------------------------------- 1 | package httpmiddleware 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "sync" 7 | 8 | tracesdk "go.opentelemetry.io/otel/sdk/trace" 9 | "go.opentelemetry.io/otel/sdk/trace/tracetest" 10 | "go.opentelemetry.io/otel/trace" 11 | ) 12 | 13 | type randomIDGenerator struct { 14 | sync.Mutex 15 | rand *rand.Rand 16 | } 17 | 18 | // NewSpanID returns a non-zero span ID from a randomly-chosen sequence. 19 | func (gen *randomIDGenerator) NewSpanID(context.Context, trace.TraceID) (sid trace.SpanID) { 20 | gen.Lock() 21 | defer gen.Unlock() 22 | gen.rand.Read(sid[:]) // #nosec G104 23 | return sid 24 | } 25 | 26 | // NewIDs returns a non-zero trace ID and a non-zero span ID from a 27 | // randomly-chosen sequence. 28 | func (gen *randomIDGenerator) NewIDs(context.Context) (tid trace.TraceID, sid trace.SpanID) { 29 | gen.Lock() 30 | defer gen.Unlock() 31 | gen.rand.Read(tid[:]) // #nosec G104 32 | gen.rand.Read(sid[:]) // #nosec G104 33 | return tid, sid 34 | } 35 | 36 | // Provider is a helper for tests providing a TracerProvider and an 37 | // InMemoryExporter. 38 | type Provider struct { 39 | *tracesdk.TracerProvider 40 | Exporter *tracetest.InMemoryExporter 41 | } 42 | 43 | // Reset clears the current in-memory storage. 44 | func (p *Provider) Reset() { 45 | p.Exporter.Reset() 46 | } 47 | 48 | // Flush forces a flush of all finished spans. 49 | func (p *Provider) Flush() { 50 | if err := p.TracerProvider.ForceFlush(context.Background()); err != nil { 51 | panic(err) 52 | } 53 | } 54 | 55 | // NewProvider initializes and returns a new Provider along with an exporter. 56 | func NewProvider() *Provider { 57 | exporter := tracetest.NewInMemoryExporter() 58 | randSource := rand.NewSource(10) 59 | tp := tracesdk.NewTracerProvider( 60 | // Using deterministic random ids. 61 | tracesdk.WithIDGenerator(&randomIDGenerator{ 62 | // #nosec G404 63 | rand: rand.New(randSource), 64 | }), 65 | tracesdk.WithBatcher(exporter, 66 | tracesdk.WithBatchTimeout(0), // instant 67 | ), 68 | ) 69 | return &Provider{ 70 | Exporter: exporter, 71 | TracerProvider: tp, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/oas/oas_cfg_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "net/http" 7 | 8 | "go.opentelemetry.io/otel" 9 | "go.opentelemetry.io/otel/metric" 10 | "go.opentelemetry.io/otel/trace" 11 | 12 | ht "github.com/ogen-go/ogen/http" 13 | "github.com/ogen-go/ogen/middleware" 14 | "github.com/ogen-go/ogen/ogenerrors" 15 | "github.com/ogen-go/ogen/otelogen" 16 | ) 17 | 18 | var ( 19 | // Allocate option closure once. 20 | clientSpanKind = trace.WithSpanKind(trace.SpanKindClient) 21 | // Allocate option closure once. 22 | serverSpanKind = trace.WithSpanKind(trace.SpanKindServer) 23 | ) 24 | 25 | type ( 26 | optionFunc[C any] func(*C) 27 | otelOptionFunc func(*otelConfig) 28 | ) 29 | 30 | type otelConfig struct { 31 | TracerProvider trace.TracerProvider 32 | Tracer trace.Tracer 33 | MeterProvider metric.MeterProvider 34 | Meter metric.Meter 35 | } 36 | 37 | func (cfg *otelConfig) initOTEL() { 38 | if cfg.TracerProvider == nil { 39 | cfg.TracerProvider = otel.GetTracerProvider() 40 | } 41 | if cfg.MeterProvider == nil { 42 | cfg.MeterProvider = otel.GetMeterProvider() 43 | } 44 | cfg.Tracer = cfg.TracerProvider.Tracer(otelogen.Name, 45 | trace.WithInstrumentationVersion(otelogen.SemVersion()), 46 | ) 47 | cfg.Meter = cfg.MeterProvider.Meter(otelogen.Name, 48 | metric.WithInstrumentationVersion(otelogen.SemVersion()), 49 | ) 50 | } 51 | 52 | // ErrorHandler is error handler. 53 | type ErrorHandler = ogenerrors.ErrorHandler 54 | 55 | type serverConfig struct { 56 | otelConfig 57 | NotFound http.HandlerFunc 58 | MethodNotAllowed func(w http.ResponseWriter, r *http.Request, allowed string) 59 | ErrorHandler ErrorHandler 60 | Prefix string 61 | Middleware Middleware 62 | MaxMultipartMemory int64 63 | } 64 | 65 | // ServerOption is server config option. 66 | type ServerOption interface { 67 | applyServer(*serverConfig) 68 | } 69 | 70 | var _ ServerOption = (optionFunc[serverConfig])(nil) 71 | 72 | func (o optionFunc[C]) applyServer(c *C) { 73 | o(c) 74 | } 75 | 76 | var _ ServerOption = (otelOptionFunc)(nil) 77 | 78 | func (o otelOptionFunc) applyServer(c *serverConfig) { 79 | o(&c.otelConfig) 80 | } 81 | 82 | func newServerConfig(opts ...ServerOption) serverConfig { 83 | cfg := serverConfig{ 84 | NotFound: http.NotFound, 85 | MethodNotAllowed: func(w http.ResponseWriter, r *http.Request, allowed string) { 86 | status := http.StatusMethodNotAllowed 87 | if r.Method == "OPTIONS" { 88 | w.Header().Set("Access-Control-Allow-Methods", allowed) 89 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type") 90 | status = http.StatusNoContent 91 | } else { 92 | w.Header().Set("Allow", allowed) 93 | } 94 | w.WriteHeader(status) 95 | }, 96 | ErrorHandler: ogenerrors.DefaultErrorHandler, 97 | Middleware: nil, 98 | MaxMultipartMemory: 32 << 20, // 32 MB 99 | } 100 | for _, opt := range opts { 101 | opt.applyServer(&cfg) 102 | } 103 | cfg.initOTEL() 104 | return cfg 105 | } 106 | 107 | type baseServer struct { 108 | cfg serverConfig 109 | requests metric.Int64Counter 110 | errors metric.Int64Counter 111 | duration metric.Float64Histogram 112 | } 113 | 114 | func (s baseServer) notFound(w http.ResponseWriter, r *http.Request) { 115 | s.cfg.NotFound(w, r) 116 | } 117 | 118 | func (s baseServer) notAllowed(w http.ResponseWriter, r *http.Request, allowed string) { 119 | s.cfg.MethodNotAllowed(w, r, allowed) 120 | } 121 | 122 | func (cfg serverConfig) baseServer() (s baseServer, err error) { 123 | s = baseServer{cfg: cfg} 124 | if s.requests, err = otelogen.ServerRequestCountCounter(s.cfg.Meter); err != nil { 125 | return s, err 126 | } 127 | if s.errors, err = otelogen.ServerErrorsCountCounter(s.cfg.Meter); err != nil { 128 | return s, err 129 | } 130 | if s.duration, err = otelogen.ServerDurationHistogram(s.cfg.Meter); err != nil { 131 | return s, err 132 | } 133 | return s, nil 134 | } 135 | 136 | type clientConfig struct { 137 | otelConfig 138 | Client ht.Client 139 | } 140 | 141 | // ClientOption is client config option. 142 | type ClientOption interface { 143 | applyClient(*clientConfig) 144 | } 145 | 146 | var _ ClientOption = (optionFunc[clientConfig])(nil) 147 | 148 | func (o optionFunc[C]) applyClient(c *C) { 149 | o(c) 150 | } 151 | 152 | var _ ClientOption = (otelOptionFunc)(nil) 153 | 154 | func (o otelOptionFunc) applyClient(c *clientConfig) { 155 | o(&c.otelConfig) 156 | } 157 | 158 | func newClientConfig(opts ...ClientOption) clientConfig { 159 | cfg := clientConfig{ 160 | Client: http.DefaultClient, 161 | } 162 | for _, opt := range opts { 163 | opt.applyClient(&cfg) 164 | } 165 | cfg.initOTEL() 166 | return cfg 167 | } 168 | 169 | type baseClient struct { 170 | cfg clientConfig 171 | requests metric.Int64Counter 172 | errors metric.Int64Counter 173 | duration metric.Float64Histogram 174 | } 175 | 176 | func (cfg clientConfig) baseClient() (c baseClient, err error) { 177 | c = baseClient{cfg: cfg} 178 | if c.requests, err = otelogen.ClientRequestCountCounter(c.cfg.Meter); err != nil { 179 | return c, err 180 | } 181 | if c.errors, err = otelogen.ClientErrorsCountCounter(c.cfg.Meter); err != nil { 182 | return c, err 183 | } 184 | if c.duration, err = otelogen.ClientDurationHistogram(c.cfg.Meter); err != nil { 185 | return c, err 186 | } 187 | return c, nil 188 | } 189 | 190 | // Option is config option. 191 | type Option interface { 192 | ServerOption 193 | ClientOption 194 | } 195 | 196 | // WithTracerProvider specifies a tracer provider to use for creating a tracer. 197 | // 198 | // If none is specified, the global provider is used. 199 | func WithTracerProvider(provider trace.TracerProvider) Option { 200 | return otelOptionFunc(func(cfg *otelConfig) { 201 | if provider != nil { 202 | cfg.TracerProvider = provider 203 | } 204 | }) 205 | } 206 | 207 | // WithMeterProvider specifies a meter provider to use for creating a meter. 208 | // 209 | // If none is specified, the otel.GetMeterProvider() is used. 210 | func WithMeterProvider(provider metric.MeterProvider) Option { 211 | return otelOptionFunc(func(cfg *otelConfig) { 212 | if provider != nil { 213 | cfg.MeterProvider = provider 214 | } 215 | }) 216 | } 217 | 218 | // WithClient specifies http client to use. 219 | func WithClient(client ht.Client) ClientOption { 220 | return optionFunc[clientConfig](func(cfg *clientConfig) { 221 | if client != nil { 222 | cfg.Client = client 223 | } 224 | }) 225 | } 226 | 227 | // WithNotFound specifies Not Found handler to use. 228 | func WithNotFound(notFound http.HandlerFunc) ServerOption { 229 | return optionFunc[serverConfig](func(cfg *serverConfig) { 230 | if notFound != nil { 231 | cfg.NotFound = notFound 232 | } 233 | }) 234 | } 235 | 236 | // WithMethodNotAllowed specifies Method Not Allowed handler to use. 237 | func WithMethodNotAllowed(methodNotAllowed func(w http.ResponseWriter, r *http.Request, allowed string)) ServerOption { 238 | return optionFunc[serverConfig](func(cfg *serverConfig) { 239 | if methodNotAllowed != nil { 240 | cfg.MethodNotAllowed = methodNotAllowed 241 | } 242 | }) 243 | } 244 | 245 | // WithErrorHandler specifies error handler to use. 246 | func WithErrorHandler(h ErrorHandler) ServerOption { 247 | return optionFunc[serverConfig](func(cfg *serverConfig) { 248 | if h != nil { 249 | cfg.ErrorHandler = h 250 | } 251 | }) 252 | } 253 | 254 | // WithPathPrefix specifies server path prefix. 255 | func WithPathPrefix(prefix string) ServerOption { 256 | return optionFunc[serverConfig](func(cfg *serverConfig) { 257 | cfg.Prefix = prefix 258 | }) 259 | } 260 | 261 | // WithMiddleware specifies middlewares to use. 262 | func WithMiddleware(m ...Middleware) ServerOption { 263 | return optionFunc[serverConfig](func(cfg *serverConfig) { 264 | switch len(m) { 265 | case 0: 266 | cfg.Middleware = nil 267 | case 1: 268 | cfg.Middleware = m[0] 269 | default: 270 | cfg.Middleware = middleware.ChainMiddlewares(m...) 271 | } 272 | }) 273 | } 274 | 275 | // WithMaxMultipartMemory specifies limit of memory for storing file parts. 276 | // File parts which can't be stored in memory will be stored on disk in temporary files. 277 | func WithMaxMultipartMemory(max int64) ServerOption { 278 | return optionFunc[serverConfig](func(cfg *serverConfig) { 279 | if max > 0 { 280 | cfg.MaxMultipartMemory = max 281 | } 282 | }) 283 | } 284 | -------------------------------------------------------------------------------- /internal/oas/oas_client_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "context" 7 | "net/url" 8 | "strings" 9 | "time" 10 | 11 | "github.com/go-faster/errors" 12 | "go.opentelemetry.io/otel/attribute" 13 | "go.opentelemetry.io/otel/codes" 14 | "go.opentelemetry.io/otel/metric" 15 | semconv "go.opentelemetry.io/otel/semconv/v1.26.0" 16 | "go.opentelemetry.io/otel/trace" 17 | 18 | "github.com/ogen-go/ogen/conv" 19 | ht "github.com/ogen-go/ogen/http" 20 | "github.com/ogen-go/ogen/otelogen" 21 | "github.com/ogen-go/ogen/uri" 22 | ) 23 | 24 | func trimTrailingSlashes(u *url.URL) { 25 | u.Path = strings.TrimRight(u.Path, "/") 26 | u.RawPath = strings.TrimRight(u.RawPath, "/") 27 | } 28 | 29 | // Invoker invokes operations described by OpenAPI v3 specification. 30 | type Invoker interface { 31 | // AddPet invokes addPet operation. 32 | // 33 | // Add a new pet to the store. 34 | // 35 | // POST /pet 36 | AddPet(ctx context.Context, request *Pet) (*Pet, error) 37 | // DeletePet invokes deletePet operation. 38 | // 39 | // Deletes a pet. 40 | // 41 | // DELETE /pet/{petId} 42 | DeletePet(ctx context.Context, params DeletePetParams) error 43 | // GetPetById invokes getPetById operation. 44 | // 45 | // Returns a single pet. 46 | // 47 | // GET /pet/{petId} 48 | GetPetById(ctx context.Context, params GetPetByIdParams) (GetPetByIdRes, error) 49 | // UpdatePet invokes updatePet operation. 50 | // 51 | // Updates a pet in the store. 52 | // 53 | // POST /pet/{petId} 54 | UpdatePet(ctx context.Context, params UpdatePetParams) error 55 | } 56 | 57 | // Client implements OAS client. 58 | type Client struct { 59 | serverURL *url.URL 60 | baseClient 61 | } 62 | 63 | var _ Handler = struct { 64 | *Client 65 | }{} 66 | 67 | // NewClient initializes new Client defined by OAS. 68 | func NewClient(serverURL string, opts ...ClientOption) (*Client, error) { 69 | u, err := url.Parse(serverURL) 70 | if err != nil { 71 | return nil, err 72 | } 73 | trimTrailingSlashes(u) 74 | 75 | c, err := newClientConfig(opts...).baseClient() 76 | if err != nil { 77 | return nil, err 78 | } 79 | return &Client{ 80 | serverURL: u, 81 | baseClient: c, 82 | }, nil 83 | } 84 | 85 | type serverURLKey struct{} 86 | 87 | // WithServerURL sets context key to override server URL. 88 | func WithServerURL(ctx context.Context, u *url.URL) context.Context { 89 | return context.WithValue(ctx, serverURLKey{}, u) 90 | } 91 | 92 | func (c *Client) requestURL(ctx context.Context) *url.URL { 93 | u, ok := ctx.Value(serverURLKey{}).(*url.URL) 94 | if !ok { 95 | return c.serverURL 96 | } 97 | return u 98 | } 99 | 100 | // AddPet invokes addPet operation. 101 | // 102 | // Add a new pet to the store. 103 | // 104 | // POST /pet 105 | func (c *Client) AddPet(ctx context.Context, request *Pet) (*Pet, error) { 106 | res, err := c.sendAddPet(ctx, request) 107 | return res, err 108 | } 109 | 110 | func (c *Client) sendAddPet(ctx context.Context, request *Pet) (res *Pet, err error) { 111 | otelAttrs := []attribute.KeyValue{ 112 | otelogen.OperationID("addPet"), 113 | semconv.HTTPRequestMethodKey.String("POST"), 114 | semconv.HTTPRouteKey.String("/pet"), 115 | } 116 | 117 | // Run stopwatch. 118 | startTime := time.Now() 119 | defer func() { 120 | // Use floating point division here for higher precision (instead of Millisecond method). 121 | elapsedDuration := time.Since(startTime) 122 | c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) 123 | }() 124 | 125 | // Increment request counter. 126 | c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) 127 | 128 | // Start a span for this request. 129 | ctx, span := c.cfg.Tracer.Start(ctx, AddPetOperation, 130 | trace.WithAttributes(otelAttrs...), 131 | clientSpanKind, 132 | ) 133 | // Track stage for error reporting. 134 | var stage string 135 | defer func() { 136 | if err != nil { 137 | span.RecordError(err) 138 | span.SetStatus(codes.Error, stage) 139 | c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) 140 | } 141 | span.End() 142 | }() 143 | 144 | stage = "BuildURL" 145 | u := uri.Clone(c.requestURL(ctx)) 146 | var pathParts [1]string 147 | pathParts[0] = "/pet" 148 | uri.AddPathParts(u, pathParts[:]...) 149 | 150 | stage = "EncodeRequest" 151 | r, err := ht.NewRequest(ctx, "POST", u) 152 | if err != nil { 153 | return res, errors.Wrap(err, "create request") 154 | } 155 | if err := encodeAddPetRequest(request, r); err != nil { 156 | return res, errors.Wrap(err, "encode request") 157 | } 158 | 159 | stage = "SendRequest" 160 | resp, err := c.cfg.Client.Do(r) 161 | if err != nil { 162 | return res, errors.Wrap(err, "do request") 163 | } 164 | defer resp.Body.Close() 165 | 166 | stage = "DecodeResponse" 167 | result, err := decodeAddPetResponse(resp) 168 | if err != nil { 169 | return res, errors.Wrap(err, "decode response") 170 | } 171 | 172 | return result, nil 173 | } 174 | 175 | // DeletePet invokes deletePet operation. 176 | // 177 | // Deletes a pet. 178 | // 179 | // DELETE /pet/{petId} 180 | func (c *Client) DeletePet(ctx context.Context, params DeletePetParams) error { 181 | _, err := c.sendDeletePet(ctx, params) 182 | return err 183 | } 184 | 185 | func (c *Client) sendDeletePet(ctx context.Context, params DeletePetParams) (res *DeletePetOK, err error) { 186 | otelAttrs := []attribute.KeyValue{ 187 | otelogen.OperationID("deletePet"), 188 | semconv.HTTPRequestMethodKey.String("DELETE"), 189 | semconv.HTTPRouteKey.String("/pet/{petId}"), 190 | } 191 | 192 | // Run stopwatch. 193 | startTime := time.Now() 194 | defer func() { 195 | // Use floating point division here for higher precision (instead of Millisecond method). 196 | elapsedDuration := time.Since(startTime) 197 | c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) 198 | }() 199 | 200 | // Increment request counter. 201 | c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) 202 | 203 | // Start a span for this request. 204 | ctx, span := c.cfg.Tracer.Start(ctx, DeletePetOperation, 205 | trace.WithAttributes(otelAttrs...), 206 | clientSpanKind, 207 | ) 208 | // Track stage for error reporting. 209 | var stage string 210 | defer func() { 211 | if err != nil { 212 | span.RecordError(err) 213 | span.SetStatus(codes.Error, stage) 214 | c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) 215 | } 216 | span.End() 217 | }() 218 | 219 | stage = "BuildURL" 220 | u := uri.Clone(c.requestURL(ctx)) 221 | var pathParts [2]string 222 | pathParts[0] = "/pet/" 223 | { 224 | // Encode "petId" parameter. 225 | e := uri.NewPathEncoder(uri.PathEncoderConfig{ 226 | Param: "petId", 227 | Style: uri.PathStyleSimple, 228 | Explode: false, 229 | }) 230 | if err := func() error { 231 | return e.EncodeValue(conv.Int64ToString(params.PetId)) 232 | }(); err != nil { 233 | return res, errors.Wrap(err, "encode path") 234 | } 235 | encoded, err := e.Result() 236 | if err != nil { 237 | return res, errors.Wrap(err, "encode path") 238 | } 239 | pathParts[1] = encoded 240 | } 241 | uri.AddPathParts(u, pathParts[:]...) 242 | 243 | stage = "EncodeRequest" 244 | r, err := ht.NewRequest(ctx, "DELETE", u) 245 | if err != nil { 246 | return res, errors.Wrap(err, "create request") 247 | } 248 | 249 | stage = "SendRequest" 250 | resp, err := c.cfg.Client.Do(r) 251 | if err != nil { 252 | return res, errors.Wrap(err, "do request") 253 | } 254 | defer resp.Body.Close() 255 | 256 | stage = "DecodeResponse" 257 | result, err := decodeDeletePetResponse(resp) 258 | if err != nil { 259 | return res, errors.Wrap(err, "decode response") 260 | } 261 | 262 | return result, nil 263 | } 264 | 265 | // GetPetById invokes getPetById operation. 266 | // 267 | // Returns a single pet. 268 | // 269 | // GET /pet/{petId} 270 | func (c *Client) GetPetById(ctx context.Context, params GetPetByIdParams) (GetPetByIdRes, error) { 271 | res, err := c.sendGetPetById(ctx, params) 272 | return res, err 273 | } 274 | 275 | func (c *Client) sendGetPetById(ctx context.Context, params GetPetByIdParams) (res GetPetByIdRes, err error) { 276 | otelAttrs := []attribute.KeyValue{ 277 | otelogen.OperationID("getPetById"), 278 | semconv.HTTPRequestMethodKey.String("GET"), 279 | semconv.HTTPRouteKey.String("/pet/{petId}"), 280 | } 281 | 282 | // Run stopwatch. 283 | startTime := time.Now() 284 | defer func() { 285 | // Use floating point division here for higher precision (instead of Millisecond method). 286 | elapsedDuration := time.Since(startTime) 287 | c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) 288 | }() 289 | 290 | // Increment request counter. 291 | c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) 292 | 293 | // Start a span for this request. 294 | ctx, span := c.cfg.Tracer.Start(ctx, GetPetByIdOperation, 295 | trace.WithAttributes(otelAttrs...), 296 | clientSpanKind, 297 | ) 298 | // Track stage for error reporting. 299 | var stage string 300 | defer func() { 301 | if err != nil { 302 | span.RecordError(err) 303 | span.SetStatus(codes.Error, stage) 304 | c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) 305 | } 306 | span.End() 307 | }() 308 | 309 | stage = "BuildURL" 310 | u := uri.Clone(c.requestURL(ctx)) 311 | var pathParts [2]string 312 | pathParts[0] = "/pet/" 313 | { 314 | // Encode "petId" parameter. 315 | e := uri.NewPathEncoder(uri.PathEncoderConfig{ 316 | Param: "petId", 317 | Style: uri.PathStyleSimple, 318 | Explode: false, 319 | }) 320 | if err := func() error { 321 | return e.EncodeValue(conv.Int64ToString(params.PetId)) 322 | }(); err != nil { 323 | return res, errors.Wrap(err, "encode path") 324 | } 325 | encoded, err := e.Result() 326 | if err != nil { 327 | return res, errors.Wrap(err, "encode path") 328 | } 329 | pathParts[1] = encoded 330 | } 331 | uri.AddPathParts(u, pathParts[:]...) 332 | 333 | stage = "EncodeRequest" 334 | r, err := ht.NewRequest(ctx, "GET", u) 335 | if err != nil { 336 | return res, errors.Wrap(err, "create request") 337 | } 338 | 339 | stage = "SendRequest" 340 | resp, err := c.cfg.Client.Do(r) 341 | if err != nil { 342 | return res, errors.Wrap(err, "do request") 343 | } 344 | defer resp.Body.Close() 345 | 346 | stage = "DecodeResponse" 347 | result, err := decodeGetPetByIdResponse(resp) 348 | if err != nil { 349 | return res, errors.Wrap(err, "decode response") 350 | } 351 | 352 | return result, nil 353 | } 354 | 355 | // UpdatePet invokes updatePet operation. 356 | // 357 | // Updates a pet in the store. 358 | // 359 | // POST /pet/{petId} 360 | func (c *Client) UpdatePet(ctx context.Context, params UpdatePetParams) error { 361 | _, err := c.sendUpdatePet(ctx, params) 362 | return err 363 | } 364 | 365 | func (c *Client) sendUpdatePet(ctx context.Context, params UpdatePetParams) (res *UpdatePetOK, err error) { 366 | otelAttrs := []attribute.KeyValue{ 367 | otelogen.OperationID("updatePet"), 368 | semconv.HTTPRequestMethodKey.String("POST"), 369 | semconv.HTTPRouteKey.String("/pet/{petId}"), 370 | } 371 | 372 | // Run stopwatch. 373 | startTime := time.Now() 374 | defer func() { 375 | // Use floating point division here for higher precision (instead of Millisecond method). 376 | elapsedDuration := time.Since(startTime) 377 | c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) 378 | }() 379 | 380 | // Increment request counter. 381 | c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) 382 | 383 | // Start a span for this request. 384 | ctx, span := c.cfg.Tracer.Start(ctx, UpdatePetOperation, 385 | trace.WithAttributes(otelAttrs...), 386 | clientSpanKind, 387 | ) 388 | // Track stage for error reporting. 389 | var stage string 390 | defer func() { 391 | if err != nil { 392 | span.RecordError(err) 393 | span.SetStatus(codes.Error, stage) 394 | c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) 395 | } 396 | span.End() 397 | }() 398 | 399 | stage = "BuildURL" 400 | u := uri.Clone(c.requestURL(ctx)) 401 | var pathParts [2]string 402 | pathParts[0] = "/pet/" 403 | { 404 | // Encode "petId" parameter. 405 | e := uri.NewPathEncoder(uri.PathEncoderConfig{ 406 | Param: "petId", 407 | Style: uri.PathStyleSimple, 408 | Explode: false, 409 | }) 410 | if err := func() error { 411 | return e.EncodeValue(conv.Int64ToString(params.PetId)) 412 | }(); err != nil { 413 | return res, errors.Wrap(err, "encode path") 414 | } 415 | encoded, err := e.Result() 416 | if err != nil { 417 | return res, errors.Wrap(err, "encode path") 418 | } 419 | pathParts[1] = encoded 420 | } 421 | uri.AddPathParts(u, pathParts[:]...) 422 | 423 | stage = "EncodeQueryParams" 424 | q := uri.NewQueryEncoder() 425 | { 426 | // Encode "name" parameter. 427 | cfg := uri.QueryParameterEncodingConfig{ 428 | Name: "name", 429 | Style: uri.QueryStyleForm, 430 | Explode: true, 431 | } 432 | 433 | if err := q.EncodeParam(cfg, func(e uri.Encoder) error { 434 | if val, ok := params.Name.Get(); ok { 435 | return e.EncodeValue(conv.StringToString(val)) 436 | } 437 | return nil 438 | }); err != nil { 439 | return res, errors.Wrap(err, "encode query") 440 | } 441 | } 442 | { 443 | // Encode "status" parameter. 444 | cfg := uri.QueryParameterEncodingConfig{ 445 | Name: "status", 446 | Style: uri.QueryStyleForm, 447 | Explode: true, 448 | } 449 | 450 | if err := q.EncodeParam(cfg, func(e uri.Encoder) error { 451 | if val, ok := params.Status.Get(); ok { 452 | return e.EncodeValue(conv.StringToString(string(val))) 453 | } 454 | return nil 455 | }); err != nil { 456 | return res, errors.Wrap(err, "encode query") 457 | } 458 | } 459 | u.RawQuery = q.Values().Encode() 460 | 461 | stage = "EncodeRequest" 462 | r, err := ht.NewRequest(ctx, "POST", u) 463 | if err != nil { 464 | return res, errors.Wrap(err, "create request") 465 | } 466 | 467 | stage = "SendRequest" 468 | resp, err := c.cfg.Client.Do(r) 469 | if err != nil { 470 | return res, errors.Wrap(err, "do request") 471 | } 472 | defer resp.Body.Close() 473 | 474 | stage = "DecodeResponse" 475 | result, err := decodeUpdatePetResponse(resp) 476 | if err != nil { 477 | return res, errors.Wrap(err, "decode response") 478 | } 479 | 480 | return result, nil 481 | } 482 | -------------------------------------------------------------------------------- /internal/oas/oas_handlers_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/go-faster/errors" 11 | "go.opentelemetry.io/otel/attribute" 12 | "go.opentelemetry.io/otel/codes" 13 | "go.opentelemetry.io/otel/metric" 14 | semconv "go.opentelemetry.io/otel/semconv/v1.26.0" 15 | "go.opentelemetry.io/otel/trace" 16 | 17 | ht "github.com/ogen-go/ogen/http" 18 | "github.com/ogen-go/ogen/middleware" 19 | "github.com/ogen-go/ogen/ogenerrors" 20 | "github.com/ogen-go/ogen/otelogen" 21 | ) 22 | 23 | type codeRecorder struct { 24 | http.ResponseWriter 25 | status int 26 | } 27 | 28 | func (c *codeRecorder) WriteHeader(status int) { 29 | c.status = status 30 | c.ResponseWriter.WriteHeader(status) 31 | } 32 | 33 | // handleAddPetRequest handles addPet operation. 34 | // 35 | // Add a new pet to the store. 36 | // 37 | // POST /pet 38 | func (s *Server) handleAddPetRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { 39 | statusWriter := &codeRecorder{ResponseWriter: w} 40 | w = statusWriter 41 | otelAttrs := []attribute.KeyValue{ 42 | otelogen.OperationID("addPet"), 43 | semconv.HTTPRequestMethodKey.String("POST"), 44 | semconv.HTTPRouteKey.String("/pet"), 45 | } 46 | 47 | // Start a span for this request. 48 | ctx, span := s.cfg.Tracer.Start(r.Context(), AddPetOperation, 49 | trace.WithAttributes(otelAttrs...), 50 | serverSpanKind, 51 | ) 52 | defer span.End() 53 | 54 | // Add Labeler to context. 55 | labeler := &Labeler{attrs: otelAttrs} 56 | ctx = contextWithLabeler(ctx, labeler) 57 | 58 | // Run stopwatch. 59 | startTime := time.Now() 60 | defer func() { 61 | elapsedDuration := time.Since(startTime) 62 | 63 | attrSet := labeler.AttributeSet() 64 | attrs := attrSet.ToSlice() 65 | code := statusWriter.status 66 | if code != 0 { 67 | codeAttr := semconv.HTTPResponseStatusCode(code) 68 | attrs = append(attrs, codeAttr) 69 | span.SetAttributes(codeAttr) 70 | } 71 | attrOpt := metric.WithAttributes(attrs...) 72 | 73 | // Increment request counter. 74 | s.requests.Add(ctx, 1, attrOpt) 75 | 76 | // Use floating point division here for higher precision (instead of Millisecond method). 77 | s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) 78 | }() 79 | 80 | var ( 81 | recordError = func(stage string, err error) { 82 | span.RecordError(err) 83 | 84 | // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status 85 | // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, 86 | // unless there was another error (e.g., network error receiving the response body; or 3xx codes with 87 | // max redirects exceeded), in which case status MUST be set to Error. 88 | code := statusWriter.status 89 | if code >= 100 && code < 500 { 90 | span.SetStatus(codes.Error, stage) 91 | } 92 | 93 | attrSet := labeler.AttributeSet() 94 | attrs := attrSet.ToSlice() 95 | if code != 0 { 96 | attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) 97 | } 98 | 99 | s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) 100 | } 101 | err error 102 | opErrContext = ogenerrors.OperationContext{ 103 | Name: AddPetOperation, 104 | ID: "addPet", 105 | } 106 | ) 107 | request, close, err := s.decodeAddPetRequest(r) 108 | if err != nil { 109 | err = &ogenerrors.DecodeRequestError{ 110 | OperationContext: opErrContext, 111 | Err: err, 112 | } 113 | defer recordError("DecodeRequest", err) 114 | s.cfg.ErrorHandler(ctx, w, r, err) 115 | return 116 | } 117 | defer func() { 118 | if err := close(); err != nil { 119 | recordError("CloseRequest", err) 120 | } 121 | }() 122 | 123 | var response *Pet 124 | if m := s.cfg.Middleware; m != nil { 125 | mreq := middleware.Request{ 126 | Context: ctx, 127 | OperationName: AddPetOperation, 128 | OperationSummary: "Add a new pet to the store", 129 | OperationID: "addPet", 130 | Body: request, 131 | Params: middleware.Parameters{}, 132 | Raw: r, 133 | } 134 | 135 | type ( 136 | Request = *Pet 137 | Params = struct{} 138 | Response = *Pet 139 | ) 140 | response, err = middleware.HookMiddleware[ 141 | Request, 142 | Params, 143 | Response, 144 | ]( 145 | m, 146 | mreq, 147 | nil, 148 | func(ctx context.Context, request Request, params Params) (response Response, err error) { 149 | response, err = s.h.AddPet(ctx, request) 150 | return response, err 151 | }, 152 | ) 153 | } else { 154 | response, err = s.h.AddPet(ctx, request) 155 | } 156 | if err != nil { 157 | defer recordError("Internal", err) 158 | s.cfg.ErrorHandler(ctx, w, r, err) 159 | return 160 | } 161 | 162 | if err := encodeAddPetResponse(response, w, span); err != nil { 163 | defer recordError("EncodeResponse", err) 164 | if !errors.Is(err, ht.ErrInternalServerErrorResponse) { 165 | s.cfg.ErrorHandler(ctx, w, r, err) 166 | } 167 | return 168 | } 169 | } 170 | 171 | // handleDeletePetRequest handles deletePet operation. 172 | // 173 | // Deletes a pet. 174 | // 175 | // DELETE /pet/{petId} 176 | func (s *Server) handleDeletePetRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { 177 | statusWriter := &codeRecorder{ResponseWriter: w} 178 | w = statusWriter 179 | otelAttrs := []attribute.KeyValue{ 180 | otelogen.OperationID("deletePet"), 181 | semconv.HTTPRequestMethodKey.String("DELETE"), 182 | semconv.HTTPRouteKey.String("/pet/{petId}"), 183 | } 184 | 185 | // Start a span for this request. 186 | ctx, span := s.cfg.Tracer.Start(r.Context(), DeletePetOperation, 187 | trace.WithAttributes(otelAttrs...), 188 | serverSpanKind, 189 | ) 190 | defer span.End() 191 | 192 | // Add Labeler to context. 193 | labeler := &Labeler{attrs: otelAttrs} 194 | ctx = contextWithLabeler(ctx, labeler) 195 | 196 | // Run stopwatch. 197 | startTime := time.Now() 198 | defer func() { 199 | elapsedDuration := time.Since(startTime) 200 | 201 | attrSet := labeler.AttributeSet() 202 | attrs := attrSet.ToSlice() 203 | code := statusWriter.status 204 | if code != 0 { 205 | codeAttr := semconv.HTTPResponseStatusCode(code) 206 | attrs = append(attrs, codeAttr) 207 | span.SetAttributes(codeAttr) 208 | } 209 | attrOpt := metric.WithAttributes(attrs...) 210 | 211 | // Increment request counter. 212 | s.requests.Add(ctx, 1, attrOpt) 213 | 214 | // Use floating point division here for higher precision (instead of Millisecond method). 215 | s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) 216 | }() 217 | 218 | var ( 219 | recordError = func(stage string, err error) { 220 | span.RecordError(err) 221 | 222 | // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status 223 | // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, 224 | // unless there was another error (e.g., network error receiving the response body; or 3xx codes with 225 | // max redirects exceeded), in which case status MUST be set to Error. 226 | code := statusWriter.status 227 | if code >= 100 && code < 500 { 228 | span.SetStatus(codes.Error, stage) 229 | } 230 | 231 | attrSet := labeler.AttributeSet() 232 | attrs := attrSet.ToSlice() 233 | if code != 0 { 234 | attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) 235 | } 236 | 237 | s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) 238 | } 239 | err error 240 | opErrContext = ogenerrors.OperationContext{ 241 | Name: DeletePetOperation, 242 | ID: "deletePet", 243 | } 244 | ) 245 | params, err := decodeDeletePetParams(args, argsEscaped, r) 246 | if err != nil { 247 | err = &ogenerrors.DecodeParamsError{ 248 | OperationContext: opErrContext, 249 | Err: err, 250 | } 251 | defer recordError("DecodeParams", err) 252 | s.cfg.ErrorHandler(ctx, w, r, err) 253 | return 254 | } 255 | 256 | var response *DeletePetOK 257 | if m := s.cfg.Middleware; m != nil { 258 | mreq := middleware.Request{ 259 | Context: ctx, 260 | OperationName: DeletePetOperation, 261 | OperationSummary: "Deletes a pet", 262 | OperationID: "deletePet", 263 | Body: nil, 264 | Params: middleware.Parameters{ 265 | { 266 | Name: "petId", 267 | In: "path", 268 | }: params.PetId, 269 | }, 270 | Raw: r, 271 | } 272 | 273 | type ( 274 | Request = struct{} 275 | Params = DeletePetParams 276 | Response = *DeletePetOK 277 | ) 278 | response, err = middleware.HookMiddleware[ 279 | Request, 280 | Params, 281 | Response, 282 | ]( 283 | m, 284 | mreq, 285 | unpackDeletePetParams, 286 | func(ctx context.Context, request Request, params Params) (response Response, err error) { 287 | err = s.h.DeletePet(ctx, params) 288 | return response, err 289 | }, 290 | ) 291 | } else { 292 | err = s.h.DeletePet(ctx, params) 293 | } 294 | if err != nil { 295 | defer recordError("Internal", err) 296 | s.cfg.ErrorHandler(ctx, w, r, err) 297 | return 298 | } 299 | 300 | if err := encodeDeletePetResponse(response, w, span); err != nil { 301 | defer recordError("EncodeResponse", err) 302 | if !errors.Is(err, ht.ErrInternalServerErrorResponse) { 303 | s.cfg.ErrorHandler(ctx, w, r, err) 304 | } 305 | return 306 | } 307 | } 308 | 309 | // handleGetPetByIdRequest handles getPetById operation. 310 | // 311 | // Returns a single pet. 312 | // 313 | // GET /pet/{petId} 314 | func (s *Server) handleGetPetByIdRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { 315 | statusWriter := &codeRecorder{ResponseWriter: w} 316 | w = statusWriter 317 | otelAttrs := []attribute.KeyValue{ 318 | otelogen.OperationID("getPetById"), 319 | semconv.HTTPRequestMethodKey.String("GET"), 320 | semconv.HTTPRouteKey.String("/pet/{petId}"), 321 | } 322 | 323 | // Start a span for this request. 324 | ctx, span := s.cfg.Tracer.Start(r.Context(), GetPetByIdOperation, 325 | trace.WithAttributes(otelAttrs...), 326 | serverSpanKind, 327 | ) 328 | defer span.End() 329 | 330 | // Add Labeler to context. 331 | labeler := &Labeler{attrs: otelAttrs} 332 | ctx = contextWithLabeler(ctx, labeler) 333 | 334 | // Run stopwatch. 335 | startTime := time.Now() 336 | defer func() { 337 | elapsedDuration := time.Since(startTime) 338 | 339 | attrSet := labeler.AttributeSet() 340 | attrs := attrSet.ToSlice() 341 | code := statusWriter.status 342 | if code != 0 { 343 | codeAttr := semconv.HTTPResponseStatusCode(code) 344 | attrs = append(attrs, codeAttr) 345 | span.SetAttributes(codeAttr) 346 | } 347 | attrOpt := metric.WithAttributes(attrs...) 348 | 349 | // Increment request counter. 350 | s.requests.Add(ctx, 1, attrOpt) 351 | 352 | // Use floating point division here for higher precision (instead of Millisecond method). 353 | s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) 354 | }() 355 | 356 | var ( 357 | recordError = func(stage string, err error) { 358 | span.RecordError(err) 359 | 360 | // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status 361 | // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, 362 | // unless there was another error (e.g., network error receiving the response body; or 3xx codes with 363 | // max redirects exceeded), in which case status MUST be set to Error. 364 | code := statusWriter.status 365 | if code >= 100 && code < 500 { 366 | span.SetStatus(codes.Error, stage) 367 | } 368 | 369 | attrSet := labeler.AttributeSet() 370 | attrs := attrSet.ToSlice() 371 | if code != 0 { 372 | attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) 373 | } 374 | 375 | s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) 376 | } 377 | err error 378 | opErrContext = ogenerrors.OperationContext{ 379 | Name: GetPetByIdOperation, 380 | ID: "getPetById", 381 | } 382 | ) 383 | params, err := decodeGetPetByIdParams(args, argsEscaped, r) 384 | if err != nil { 385 | err = &ogenerrors.DecodeParamsError{ 386 | OperationContext: opErrContext, 387 | Err: err, 388 | } 389 | defer recordError("DecodeParams", err) 390 | s.cfg.ErrorHandler(ctx, w, r, err) 391 | return 392 | } 393 | 394 | var response GetPetByIdRes 395 | if m := s.cfg.Middleware; m != nil { 396 | mreq := middleware.Request{ 397 | Context: ctx, 398 | OperationName: GetPetByIdOperation, 399 | OperationSummary: "Find pet by ID", 400 | OperationID: "getPetById", 401 | Body: nil, 402 | Params: middleware.Parameters{ 403 | { 404 | Name: "petId", 405 | In: "path", 406 | }: params.PetId, 407 | }, 408 | Raw: r, 409 | } 410 | 411 | type ( 412 | Request = struct{} 413 | Params = GetPetByIdParams 414 | Response = GetPetByIdRes 415 | ) 416 | response, err = middleware.HookMiddleware[ 417 | Request, 418 | Params, 419 | Response, 420 | ]( 421 | m, 422 | mreq, 423 | unpackGetPetByIdParams, 424 | func(ctx context.Context, request Request, params Params) (response Response, err error) { 425 | response, err = s.h.GetPetById(ctx, params) 426 | return response, err 427 | }, 428 | ) 429 | } else { 430 | response, err = s.h.GetPetById(ctx, params) 431 | } 432 | if err != nil { 433 | defer recordError("Internal", err) 434 | s.cfg.ErrorHandler(ctx, w, r, err) 435 | return 436 | } 437 | 438 | if err := encodeGetPetByIdResponse(response, w, span); err != nil { 439 | defer recordError("EncodeResponse", err) 440 | if !errors.Is(err, ht.ErrInternalServerErrorResponse) { 441 | s.cfg.ErrorHandler(ctx, w, r, err) 442 | } 443 | return 444 | } 445 | } 446 | 447 | // handleUpdatePetRequest handles updatePet operation. 448 | // 449 | // Updates a pet in the store. 450 | // 451 | // POST /pet/{petId} 452 | func (s *Server) handleUpdatePetRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { 453 | statusWriter := &codeRecorder{ResponseWriter: w} 454 | w = statusWriter 455 | otelAttrs := []attribute.KeyValue{ 456 | otelogen.OperationID("updatePet"), 457 | semconv.HTTPRequestMethodKey.String("POST"), 458 | semconv.HTTPRouteKey.String("/pet/{petId}"), 459 | } 460 | 461 | // Start a span for this request. 462 | ctx, span := s.cfg.Tracer.Start(r.Context(), UpdatePetOperation, 463 | trace.WithAttributes(otelAttrs...), 464 | serverSpanKind, 465 | ) 466 | defer span.End() 467 | 468 | // Add Labeler to context. 469 | labeler := &Labeler{attrs: otelAttrs} 470 | ctx = contextWithLabeler(ctx, labeler) 471 | 472 | // Run stopwatch. 473 | startTime := time.Now() 474 | defer func() { 475 | elapsedDuration := time.Since(startTime) 476 | 477 | attrSet := labeler.AttributeSet() 478 | attrs := attrSet.ToSlice() 479 | code := statusWriter.status 480 | if code != 0 { 481 | codeAttr := semconv.HTTPResponseStatusCode(code) 482 | attrs = append(attrs, codeAttr) 483 | span.SetAttributes(codeAttr) 484 | } 485 | attrOpt := metric.WithAttributes(attrs...) 486 | 487 | // Increment request counter. 488 | s.requests.Add(ctx, 1, attrOpt) 489 | 490 | // Use floating point division here for higher precision (instead of Millisecond method). 491 | s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) 492 | }() 493 | 494 | var ( 495 | recordError = func(stage string, err error) { 496 | span.RecordError(err) 497 | 498 | // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status 499 | // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, 500 | // unless there was another error (e.g., network error receiving the response body; or 3xx codes with 501 | // max redirects exceeded), in which case status MUST be set to Error. 502 | code := statusWriter.status 503 | if code >= 100 && code < 500 { 504 | span.SetStatus(codes.Error, stage) 505 | } 506 | 507 | attrSet := labeler.AttributeSet() 508 | attrs := attrSet.ToSlice() 509 | if code != 0 { 510 | attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) 511 | } 512 | 513 | s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) 514 | } 515 | err error 516 | opErrContext = ogenerrors.OperationContext{ 517 | Name: UpdatePetOperation, 518 | ID: "updatePet", 519 | } 520 | ) 521 | params, err := decodeUpdatePetParams(args, argsEscaped, r) 522 | if err != nil { 523 | err = &ogenerrors.DecodeParamsError{ 524 | OperationContext: opErrContext, 525 | Err: err, 526 | } 527 | defer recordError("DecodeParams", err) 528 | s.cfg.ErrorHandler(ctx, w, r, err) 529 | return 530 | } 531 | 532 | var response *UpdatePetOK 533 | if m := s.cfg.Middleware; m != nil { 534 | mreq := middleware.Request{ 535 | Context: ctx, 536 | OperationName: UpdatePetOperation, 537 | OperationSummary: "Updates a pet in the store", 538 | OperationID: "updatePet", 539 | Body: nil, 540 | Params: middleware.Parameters{ 541 | { 542 | Name: "petId", 543 | In: "path", 544 | }: params.PetId, 545 | { 546 | Name: "name", 547 | In: "query", 548 | }: params.Name, 549 | { 550 | Name: "status", 551 | In: "query", 552 | }: params.Status, 553 | }, 554 | Raw: r, 555 | } 556 | 557 | type ( 558 | Request = struct{} 559 | Params = UpdatePetParams 560 | Response = *UpdatePetOK 561 | ) 562 | response, err = middleware.HookMiddleware[ 563 | Request, 564 | Params, 565 | Response, 566 | ]( 567 | m, 568 | mreq, 569 | unpackUpdatePetParams, 570 | func(ctx context.Context, request Request, params Params) (response Response, err error) { 571 | err = s.h.UpdatePet(ctx, params) 572 | return response, err 573 | }, 574 | ) 575 | } else { 576 | err = s.h.UpdatePet(ctx, params) 577 | } 578 | if err != nil { 579 | defer recordError("Internal", err) 580 | s.cfg.ErrorHandler(ctx, w, r, err) 581 | return 582 | } 583 | 584 | if err := encodeUpdatePetResponse(response, w, span); err != nil { 585 | defer recordError("EncodeResponse", err) 586 | if !errors.Is(err, ht.ErrInternalServerErrorResponse) { 587 | s.cfg.ErrorHandler(ctx, w, r, err) 588 | } 589 | return 590 | } 591 | } 592 | -------------------------------------------------------------------------------- /internal/oas/oas_interfaces_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | package oas 3 | 4 | type GetPetByIdRes interface { 5 | getPetByIdRes() 6 | } 7 | -------------------------------------------------------------------------------- /internal/oas/oas_json_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "math/bits" 7 | "strconv" 8 | 9 | "github.com/go-faster/errors" 10 | "github.com/go-faster/jx" 11 | 12 | "github.com/ogen-go/ogen/validate" 13 | ) 14 | 15 | // Encode encodes int64 as json. 16 | func (o OptInt64) Encode(e *jx.Encoder) { 17 | if !o.Set { 18 | return 19 | } 20 | e.Int64(int64(o.Value)) 21 | } 22 | 23 | // Decode decodes int64 from json. 24 | func (o *OptInt64) Decode(d *jx.Decoder) error { 25 | if o == nil { 26 | return errors.New("invalid: unable to decode OptInt64 to nil") 27 | } 28 | o.Set = true 29 | v, err := d.Int64() 30 | if err != nil { 31 | return err 32 | } 33 | o.Value = int64(v) 34 | return nil 35 | } 36 | 37 | // MarshalJSON implements stdjson.Marshaler. 38 | func (s OptInt64) MarshalJSON() ([]byte, error) { 39 | e := jx.Encoder{} 40 | s.Encode(&e) 41 | return e.Bytes(), nil 42 | } 43 | 44 | // UnmarshalJSON implements stdjson.Unmarshaler. 45 | func (s *OptInt64) UnmarshalJSON(data []byte) error { 46 | d := jx.DecodeBytes(data) 47 | return s.Decode(d) 48 | } 49 | 50 | // Encode encodes PetStatus as json. 51 | func (o OptPetStatus) Encode(e *jx.Encoder) { 52 | if !o.Set { 53 | return 54 | } 55 | e.Str(string(o.Value)) 56 | } 57 | 58 | // Decode decodes PetStatus from json. 59 | func (o *OptPetStatus) Decode(d *jx.Decoder) error { 60 | if o == nil { 61 | return errors.New("invalid: unable to decode OptPetStatus to nil") 62 | } 63 | o.Set = true 64 | if err := o.Value.Decode(d); err != nil { 65 | return err 66 | } 67 | return nil 68 | } 69 | 70 | // MarshalJSON implements stdjson.Marshaler. 71 | func (s OptPetStatus) MarshalJSON() ([]byte, error) { 72 | e := jx.Encoder{} 73 | s.Encode(&e) 74 | return e.Bytes(), nil 75 | } 76 | 77 | // UnmarshalJSON implements stdjson.Unmarshaler. 78 | func (s *OptPetStatus) UnmarshalJSON(data []byte) error { 79 | d := jx.DecodeBytes(data) 80 | return s.Decode(d) 81 | } 82 | 83 | // Encode implements json.Marshaler. 84 | func (s *Pet) Encode(e *jx.Encoder) { 85 | e.ObjStart() 86 | s.encodeFields(e) 87 | e.ObjEnd() 88 | } 89 | 90 | // encodeFields encodes fields. 91 | func (s *Pet) encodeFields(e *jx.Encoder) { 92 | { 93 | if s.ID.Set { 94 | e.FieldStart("id") 95 | s.ID.Encode(e) 96 | } 97 | } 98 | { 99 | e.FieldStart("name") 100 | e.Str(s.Name) 101 | } 102 | { 103 | if s.PhotoUrls != nil { 104 | e.FieldStart("photoUrls") 105 | e.ArrStart() 106 | for _, elem := range s.PhotoUrls { 107 | e.Str(elem) 108 | } 109 | e.ArrEnd() 110 | } 111 | } 112 | { 113 | if s.Status.Set { 114 | e.FieldStart("status") 115 | s.Status.Encode(e) 116 | } 117 | } 118 | } 119 | 120 | var jsonFieldsNameOfPet = [4]string{ 121 | 0: "id", 122 | 1: "name", 123 | 2: "photoUrls", 124 | 3: "status", 125 | } 126 | 127 | // Decode decodes Pet from json. 128 | func (s *Pet) Decode(d *jx.Decoder) error { 129 | if s == nil { 130 | return errors.New("invalid: unable to decode Pet to nil") 131 | } 132 | var requiredBitSet [1]uint8 133 | 134 | if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { 135 | switch string(k) { 136 | case "id": 137 | if err := func() error { 138 | s.ID.Reset() 139 | if err := s.ID.Decode(d); err != nil { 140 | return err 141 | } 142 | return nil 143 | }(); err != nil { 144 | return errors.Wrap(err, "decode field \"id\"") 145 | } 146 | case "name": 147 | requiredBitSet[0] |= 1 << 1 148 | if err := func() error { 149 | v, err := d.Str() 150 | s.Name = string(v) 151 | if err != nil { 152 | return err 153 | } 154 | return nil 155 | }(); err != nil { 156 | return errors.Wrap(err, "decode field \"name\"") 157 | } 158 | case "photoUrls": 159 | if err := func() error { 160 | s.PhotoUrls = make([]string, 0) 161 | if err := d.Arr(func(d *jx.Decoder) error { 162 | var elem string 163 | v, err := d.Str() 164 | elem = string(v) 165 | if err != nil { 166 | return err 167 | } 168 | s.PhotoUrls = append(s.PhotoUrls, elem) 169 | return nil 170 | }); err != nil { 171 | return err 172 | } 173 | return nil 174 | }(); err != nil { 175 | return errors.Wrap(err, "decode field \"photoUrls\"") 176 | } 177 | case "status": 178 | if err := func() error { 179 | s.Status.Reset() 180 | if err := s.Status.Decode(d); err != nil { 181 | return err 182 | } 183 | return nil 184 | }(); err != nil { 185 | return errors.Wrap(err, "decode field \"status\"") 186 | } 187 | default: 188 | return d.Skip() 189 | } 190 | return nil 191 | }); err != nil { 192 | return errors.Wrap(err, "decode Pet") 193 | } 194 | // Validate required fields. 195 | var failures []validate.FieldError 196 | for i, mask := range [1]uint8{ 197 | 0b00000010, 198 | } { 199 | if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { 200 | // Mask only required fields and check equality to mask using XOR. 201 | // 202 | // If XOR result is not zero, result is not equal to expected, so some fields are missed. 203 | // Bits of fields which would be set are actually bits of missed fields. 204 | missed := bits.OnesCount8(result) 205 | for bitN := 0; bitN < missed; bitN++ { 206 | bitIdx := bits.TrailingZeros8(result) 207 | fieldIdx := i*8 + bitIdx 208 | var name string 209 | if fieldIdx < len(jsonFieldsNameOfPet) { 210 | name = jsonFieldsNameOfPet[fieldIdx] 211 | } else { 212 | name = strconv.Itoa(fieldIdx) 213 | } 214 | failures = append(failures, validate.FieldError{ 215 | Name: name, 216 | Error: validate.ErrFieldRequired, 217 | }) 218 | // Reset bit. 219 | result &^= 1 << bitIdx 220 | } 221 | } 222 | } 223 | if len(failures) > 0 { 224 | return &validate.Error{Fields: failures} 225 | } 226 | 227 | return nil 228 | } 229 | 230 | // MarshalJSON implements stdjson.Marshaler. 231 | func (s *Pet) MarshalJSON() ([]byte, error) { 232 | e := jx.Encoder{} 233 | s.Encode(&e) 234 | return e.Bytes(), nil 235 | } 236 | 237 | // UnmarshalJSON implements stdjson.Unmarshaler. 238 | func (s *Pet) UnmarshalJSON(data []byte) error { 239 | d := jx.DecodeBytes(data) 240 | return s.Decode(d) 241 | } 242 | 243 | // Encode encodes PetStatus as json. 244 | func (s PetStatus) Encode(e *jx.Encoder) { 245 | e.Str(string(s)) 246 | } 247 | 248 | // Decode decodes PetStatus from json. 249 | func (s *PetStatus) Decode(d *jx.Decoder) error { 250 | if s == nil { 251 | return errors.New("invalid: unable to decode PetStatus to nil") 252 | } 253 | v, err := d.StrBytes() 254 | if err != nil { 255 | return err 256 | } 257 | // Try to use constant string. 258 | switch PetStatus(v) { 259 | case PetStatusAvailable: 260 | *s = PetStatusAvailable 261 | case PetStatusPending: 262 | *s = PetStatusPending 263 | case PetStatusSold: 264 | *s = PetStatusSold 265 | default: 266 | *s = PetStatus(v) 267 | } 268 | 269 | return nil 270 | } 271 | 272 | // MarshalJSON implements stdjson.Marshaler. 273 | func (s PetStatus) MarshalJSON() ([]byte, error) { 274 | e := jx.Encoder{} 275 | s.Encode(&e) 276 | return e.Bytes(), nil 277 | } 278 | 279 | // UnmarshalJSON implements stdjson.Unmarshaler. 280 | func (s *PetStatus) UnmarshalJSON(data []byte) error { 281 | d := jx.DecodeBytes(data) 282 | return s.Decode(d) 283 | } 284 | -------------------------------------------------------------------------------- /internal/oas/oas_labeler_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "context" 7 | 8 | "go.opentelemetry.io/otel/attribute" 9 | ) 10 | 11 | // Labeler is used to allow adding custom attributes to the server request metrics. 12 | type Labeler struct { 13 | attrs []attribute.KeyValue 14 | } 15 | 16 | // Add attributes to the Labeler. 17 | func (l *Labeler) Add(attrs ...attribute.KeyValue) { 18 | l.attrs = append(l.attrs, attrs...) 19 | } 20 | 21 | // AttributeSet returns the attributes added to the Labeler as an attribute.Set. 22 | func (l *Labeler) AttributeSet() attribute.Set { 23 | return attribute.NewSet(l.attrs...) 24 | } 25 | 26 | type labelerContextKey struct{} 27 | 28 | // LabelerFromContext retrieves the Labeler from the provided context, if present. 29 | // 30 | // If no Labeler was found in the provided context a new, empty Labeler is returned and the second 31 | // return value is false. In this case it is safe to use the Labeler but any attributes added to 32 | // it will not be used. 33 | func LabelerFromContext(ctx context.Context) (*Labeler, bool) { 34 | if l, ok := ctx.Value(labelerContextKey{}).(*Labeler); ok { 35 | return l, true 36 | } 37 | return &Labeler{}, false 38 | } 39 | 40 | func contextWithLabeler(ctx context.Context, l *Labeler) context.Context { 41 | return context.WithValue(ctx, labelerContextKey{}, l) 42 | } 43 | -------------------------------------------------------------------------------- /internal/oas/oas_middleware_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "github.com/ogen-go/ogen/middleware" 7 | ) 8 | 9 | // Middleware is middleware type. 10 | type Middleware = middleware.Middleware 11 | -------------------------------------------------------------------------------- /internal/oas/oas_operations_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | // OperationName is the ogen operation name 6 | type OperationName = string 7 | 8 | const ( 9 | AddPetOperation OperationName = "AddPet" 10 | DeletePetOperation OperationName = "DeletePet" 11 | GetPetByIdOperation OperationName = "GetPetById" 12 | UpdatePetOperation OperationName = "UpdatePet" 13 | ) 14 | -------------------------------------------------------------------------------- /internal/oas/oas_parameters_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "net/http" 7 | "net/url" 8 | 9 | "github.com/go-faster/errors" 10 | 11 | "github.com/ogen-go/ogen/conv" 12 | "github.com/ogen-go/ogen/middleware" 13 | "github.com/ogen-go/ogen/ogenerrors" 14 | "github.com/ogen-go/ogen/uri" 15 | "github.com/ogen-go/ogen/validate" 16 | ) 17 | 18 | // DeletePetParams is parameters of deletePet operation. 19 | type DeletePetParams struct { 20 | // Pet id to delete. 21 | PetId int64 22 | } 23 | 24 | func unpackDeletePetParams(packed middleware.Parameters) (params DeletePetParams) { 25 | { 26 | key := middleware.ParameterKey{ 27 | Name: "petId", 28 | In: "path", 29 | } 30 | params.PetId = packed[key].(int64) 31 | } 32 | return params 33 | } 34 | 35 | func decodeDeletePetParams(args [1]string, argsEscaped bool, r *http.Request) (params DeletePetParams, _ error) { 36 | // Decode path: petId. 37 | if err := func() error { 38 | param := args[0] 39 | if argsEscaped { 40 | unescaped, err := url.PathUnescape(args[0]) 41 | if err != nil { 42 | return errors.Wrap(err, "unescape path") 43 | } 44 | param = unescaped 45 | } 46 | if len(param) > 0 { 47 | d := uri.NewPathDecoder(uri.PathDecoderConfig{ 48 | Param: "petId", 49 | Value: param, 50 | Style: uri.PathStyleSimple, 51 | Explode: false, 52 | }) 53 | 54 | if err := func() error { 55 | val, err := d.DecodeValue() 56 | if err != nil { 57 | return err 58 | } 59 | 60 | c, err := conv.ToInt64(val) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | params.PetId = c 66 | return nil 67 | }(); err != nil { 68 | return err 69 | } 70 | } else { 71 | return validate.ErrFieldRequired 72 | } 73 | return nil 74 | }(); err != nil { 75 | return params, &ogenerrors.DecodeParamError{ 76 | Name: "petId", 77 | In: "path", 78 | Err: err, 79 | } 80 | } 81 | return params, nil 82 | } 83 | 84 | // GetPetByIdParams is parameters of getPetById operation. 85 | type GetPetByIdParams struct { 86 | // ID of pet to return. 87 | PetId int64 88 | } 89 | 90 | func unpackGetPetByIdParams(packed middleware.Parameters) (params GetPetByIdParams) { 91 | { 92 | key := middleware.ParameterKey{ 93 | Name: "petId", 94 | In: "path", 95 | } 96 | params.PetId = packed[key].(int64) 97 | } 98 | return params 99 | } 100 | 101 | func decodeGetPetByIdParams(args [1]string, argsEscaped bool, r *http.Request) (params GetPetByIdParams, _ error) { 102 | // Decode path: petId. 103 | if err := func() error { 104 | param := args[0] 105 | if argsEscaped { 106 | unescaped, err := url.PathUnescape(args[0]) 107 | if err != nil { 108 | return errors.Wrap(err, "unescape path") 109 | } 110 | param = unescaped 111 | } 112 | if len(param) > 0 { 113 | d := uri.NewPathDecoder(uri.PathDecoderConfig{ 114 | Param: "petId", 115 | Value: param, 116 | Style: uri.PathStyleSimple, 117 | Explode: false, 118 | }) 119 | 120 | if err := func() error { 121 | val, err := d.DecodeValue() 122 | if err != nil { 123 | return err 124 | } 125 | 126 | c, err := conv.ToInt64(val) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | params.PetId = c 132 | return nil 133 | }(); err != nil { 134 | return err 135 | } 136 | } else { 137 | return validate.ErrFieldRequired 138 | } 139 | return nil 140 | }(); err != nil { 141 | return params, &ogenerrors.DecodeParamError{ 142 | Name: "petId", 143 | In: "path", 144 | Err: err, 145 | } 146 | } 147 | return params, nil 148 | } 149 | 150 | // UpdatePetParams is parameters of updatePet operation. 151 | type UpdatePetParams struct { 152 | // ID of pet that needs to be updated. 153 | PetId int64 154 | // Name of pet that needs to be updated. 155 | Name OptString 156 | // Status of pet that needs to be updated. 157 | Status OptPetStatus 158 | } 159 | 160 | func unpackUpdatePetParams(packed middleware.Parameters) (params UpdatePetParams) { 161 | { 162 | key := middleware.ParameterKey{ 163 | Name: "petId", 164 | In: "path", 165 | } 166 | params.PetId = packed[key].(int64) 167 | } 168 | { 169 | key := middleware.ParameterKey{ 170 | Name: "name", 171 | In: "query", 172 | } 173 | if v, ok := packed[key]; ok { 174 | params.Name = v.(OptString) 175 | } 176 | } 177 | { 178 | key := middleware.ParameterKey{ 179 | Name: "status", 180 | In: "query", 181 | } 182 | if v, ok := packed[key]; ok { 183 | params.Status = v.(OptPetStatus) 184 | } 185 | } 186 | return params 187 | } 188 | 189 | func decodeUpdatePetParams(args [1]string, argsEscaped bool, r *http.Request) (params UpdatePetParams, _ error) { 190 | q := uri.NewQueryDecoder(r.URL.Query()) 191 | // Decode path: petId. 192 | if err := func() error { 193 | param := args[0] 194 | if argsEscaped { 195 | unescaped, err := url.PathUnescape(args[0]) 196 | if err != nil { 197 | return errors.Wrap(err, "unescape path") 198 | } 199 | param = unescaped 200 | } 201 | if len(param) > 0 { 202 | d := uri.NewPathDecoder(uri.PathDecoderConfig{ 203 | Param: "petId", 204 | Value: param, 205 | Style: uri.PathStyleSimple, 206 | Explode: false, 207 | }) 208 | 209 | if err := func() error { 210 | val, err := d.DecodeValue() 211 | if err != nil { 212 | return err 213 | } 214 | 215 | c, err := conv.ToInt64(val) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | params.PetId = c 221 | return nil 222 | }(); err != nil { 223 | return err 224 | } 225 | } else { 226 | return validate.ErrFieldRequired 227 | } 228 | return nil 229 | }(); err != nil { 230 | return params, &ogenerrors.DecodeParamError{ 231 | Name: "petId", 232 | In: "path", 233 | Err: err, 234 | } 235 | } 236 | // Decode query: name. 237 | if err := func() error { 238 | cfg := uri.QueryParameterDecodingConfig{ 239 | Name: "name", 240 | Style: uri.QueryStyleForm, 241 | Explode: true, 242 | } 243 | 244 | if err := q.HasParam(cfg); err == nil { 245 | if err := q.DecodeParam(cfg, func(d uri.Decoder) error { 246 | var paramsDotNameVal string 247 | if err := func() error { 248 | val, err := d.DecodeValue() 249 | if err != nil { 250 | return err 251 | } 252 | 253 | c, err := conv.ToString(val) 254 | if err != nil { 255 | return err 256 | } 257 | 258 | paramsDotNameVal = c 259 | return nil 260 | }(); err != nil { 261 | return err 262 | } 263 | params.Name.SetTo(paramsDotNameVal) 264 | return nil 265 | }); err != nil { 266 | return err 267 | } 268 | } 269 | return nil 270 | }(); err != nil { 271 | return params, &ogenerrors.DecodeParamError{ 272 | Name: "name", 273 | In: "query", 274 | Err: err, 275 | } 276 | } 277 | // Decode query: status. 278 | if err := func() error { 279 | cfg := uri.QueryParameterDecodingConfig{ 280 | Name: "status", 281 | Style: uri.QueryStyleForm, 282 | Explode: true, 283 | } 284 | 285 | if err := q.HasParam(cfg); err == nil { 286 | if err := q.DecodeParam(cfg, func(d uri.Decoder) error { 287 | var paramsDotStatusVal PetStatus 288 | if err := func() error { 289 | val, err := d.DecodeValue() 290 | if err != nil { 291 | return err 292 | } 293 | 294 | c, err := conv.ToString(val) 295 | if err != nil { 296 | return err 297 | } 298 | 299 | paramsDotStatusVal = PetStatus(c) 300 | return nil 301 | }(); err != nil { 302 | return err 303 | } 304 | params.Status.SetTo(paramsDotStatusVal) 305 | return nil 306 | }); err != nil { 307 | return err 308 | } 309 | if err := func() error { 310 | if value, ok := params.Status.Get(); ok { 311 | if err := func() error { 312 | if err := value.Validate(); err != nil { 313 | return err 314 | } 315 | return nil 316 | }(); err != nil { 317 | return err 318 | } 319 | } 320 | return nil 321 | }(); err != nil { 322 | return err 323 | } 324 | } 325 | return nil 326 | }(); err != nil { 327 | return params, &ogenerrors.DecodeParamError{ 328 | Name: "status", 329 | In: "query", 330 | Err: err, 331 | } 332 | } 333 | return params, nil 334 | } 335 | -------------------------------------------------------------------------------- /internal/oas/oas_request_decoders_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "io" 7 | "mime" 8 | "net/http" 9 | 10 | "github.com/go-faster/errors" 11 | "github.com/go-faster/jx" 12 | "go.uber.org/multierr" 13 | 14 | "github.com/ogen-go/ogen/ogenerrors" 15 | "github.com/ogen-go/ogen/validate" 16 | ) 17 | 18 | func (s *Server) decodeAddPetRequest(r *http.Request) ( 19 | req *Pet, 20 | close func() error, 21 | rerr error, 22 | ) { 23 | var closers []func() error 24 | close = func() error { 25 | var merr error 26 | // Close in reverse order, to match defer behavior. 27 | for i := len(closers) - 1; i >= 0; i-- { 28 | c := closers[i] 29 | merr = multierr.Append(merr, c()) 30 | } 31 | return merr 32 | } 33 | defer func() { 34 | if rerr != nil { 35 | rerr = multierr.Append(rerr, close()) 36 | } 37 | }() 38 | ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 39 | if err != nil { 40 | return req, close, errors.Wrap(err, "parse media type") 41 | } 42 | switch { 43 | case ct == "application/json": 44 | if r.ContentLength == 0 { 45 | return req, close, validate.ErrBodyRequired 46 | } 47 | buf, err := io.ReadAll(r.Body) 48 | if err != nil { 49 | return req, close, err 50 | } 51 | 52 | if len(buf) == 0 { 53 | return req, close, validate.ErrBodyRequired 54 | } 55 | 56 | d := jx.DecodeBytes(buf) 57 | 58 | var request Pet 59 | if err := func() error { 60 | if err := request.Decode(d); err != nil { 61 | return err 62 | } 63 | if err := d.Skip(); err != io.EOF { 64 | return errors.New("unexpected trailing data") 65 | } 66 | return nil 67 | }(); err != nil { 68 | err = &ogenerrors.DecodeBodyError{ 69 | ContentType: ct, 70 | Body: buf, 71 | Err: err, 72 | } 73 | return req, close, err 74 | } 75 | if err := func() error { 76 | if err := request.Validate(); err != nil { 77 | return err 78 | } 79 | return nil 80 | }(); err != nil { 81 | return req, close, errors.Wrap(err, "validate") 82 | } 83 | return &request, close, nil 84 | default: 85 | return req, close, validate.InvalidContentType(ct) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/oas/oas_request_encoders_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "bytes" 7 | "net/http" 8 | 9 | "github.com/go-faster/jx" 10 | 11 | ht "github.com/ogen-go/ogen/http" 12 | ) 13 | 14 | func encodeAddPetRequest( 15 | req *Pet, 16 | r *http.Request, 17 | ) error { 18 | const contentType = "application/json" 19 | e := new(jx.Encoder) 20 | { 21 | req.Encode(e) 22 | } 23 | encoded := e.Bytes() 24 | ht.SetBody(r, bytes.NewReader(encoded), contentType) 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/oas/oas_response_decoders_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "io" 7 | "mime" 8 | "net/http" 9 | 10 | "github.com/go-faster/errors" 11 | "github.com/go-faster/jx" 12 | 13 | "github.com/ogen-go/ogen/ogenerrors" 14 | "github.com/ogen-go/ogen/validate" 15 | ) 16 | 17 | func decodeAddPetResponse(resp *http.Response) (res *Pet, _ error) { 18 | switch resp.StatusCode { 19 | case 200: 20 | // Code 200. 21 | ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) 22 | if err != nil { 23 | return res, errors.Wrap(err, "parse media type") 24 | } 25 | switch { 26 | case ct == "application/json": 27 | buf, err := io.ReadAll(resp.Body) 28 | if err != nil { 29 | return res, err 30 | } 31 | d := jx.DecodeBytes(buf) 32 | 33 | var response Pet 34 | if err := func() error { 35 | if err := response.Decode(d); err != nil { 36 | return err 37 | } 38 | if err := d.Skip(); err != io.EOF { 39 | return errors.New("unexpected trailing data") 40 | } 41 | return nil 42 | }(); err != nil { 43 | err = &ogenerrors.DecodeBodyError{ 44 | ContentType: ct, 45 | Body: buf, 46 | Err: err, 47 | } 48 | return res, err 49 | } 50 | // Validate response. 51 | if err := func() error { 52 | if err := response.Validate(); err != nil { 53 | return err 54 | } 55 | return nil 56 | }(); err != nil { 57 | return res, errors.Wrap(err, "validate") 58 | } 59 | return &response, nil 60 | default: 61 | return res, validate.InvalidContentType(ct) 62 | } 63 | } 64 | return res, validate.UnexpectedStatusCode(resp.StatusCode) 65 | } 66 | 67 | func decodeDeletePetResponse(resp *http.Response) (res *DeletePetOK, _ error) { 68 | switch resp.StatusCode { 69 | case 200: 70 | // Code 200. 71 | return &DeletePetOK{}, nil 72 | } 73 | return res, validate.UnexpectedStatusCode(resp.StatusCode) 74 | } 75 | 76 | func decodeGetPetByIdResponse(resp *http.Response) (res GetPetByIdRes, _ error) { 77 | switch resp.StatusCode { 78 | case 200: 79 | // Code 200. 80 | ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) 81 | if err != nil { 82 | return res, errors.Wrap(err, "parse media type") 83 | } 84 | switch { 85 | case ct == "application/json": 86 | buf, err := io.ReadAll(resp.Body) 87 | if err != nil { 88 | return res, err 89 | } 90 | d := jx.DecodeBytes(buf) 91 | 92 | var response Pet 93 | if err := func() error { 94 | if err := response.Decode(d); err != nil { 95 | return err 96 | } 97 | if err := d.Skip(); err != io.EOF { 98 | return errors.New("unexpected trailing data") 99 | } 100 | return nil 101 | }(); err != nil { 102 | err = &ogenerrors.DecodeBodyError{ 103 | ContentType: ct, 104 | Body: buf, 105 | Err: err, 106 | } 107 | return res, err 108 | } 109 | // Validate response. 110 | if err := func() error { 111 | if err := response.Validate(); err != nil { 112 | return err 113 | } 114 | return nil 115 | }(); err != nil { 116 | return res, errors.Wrap(err, "validate") 117 | } 118 | return &response, nil 119 | default: 120 | return res, validate.InvalidContentType(ct) 121 | } 122 | case 404: 123 | // Code 404. 124 | return &GetPetByIdNotFound{}, nil 125 | } 126 | return res, validate.UnexpectedStatusCode(resp.StatusCode) 127 | } 128 | 129 | func decodeUpdatePetResponse(resp *http.Response) (res *UpdatePetOK, _ error) { 130 | switch resp.StatusCode { 131 | case 200: 132 | // Code 200. 133 | return &UpdatePetOK{}, nil 134 | } 135 | return res, validate.UnexpectedStatusCode(resp.StatusCode) 136 | } 137 | -------------------------------------------------------------------------------- /internal/oas/oas_response_encoders_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/go-faster/errors" 9 | "github.com/go-faster/jx" 10 | "go.opentelemetry.io/otel/codes" 11 | "go.opentelemetry.io/otel/trace" 12 | ) 13 | 14 | func encodeAddPetResponse(response *Pet, w http.ResponseWriter, span trace.Span) error { 15 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 16 | w.WriteHeader(200) 17 | span.SetStatus(codes.Ok, http.StatusText(200)) 18 | 19 | e := new(jx.Encoder) 20 | response.Encode(e) 21 | if _, err := e.WriteTo(w); err != nil { 22 | return errors.Wrap(err, "write") 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func encodeDeletePetResponse(response *DeletePetOK, w http.ResponseWriter, span trace.Span) error { 29 | w.WriteHeader(200) 30 | span.SetStatus(codes.Ok, http.StatusText(200)) 31 | 32 | return nil 33 | } 34 | 35 | func encodeGetPetByIdResponse(response GetPetByIdRes, w http.ResponseWriter, span trace.Span) error { 36 | switch response := response.(type) { 37 | case *Pet: 38 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 39 | w.WriteHeader(200) 40 | span.SetStatus(codes.Ok, http.StatusText(200)) 41 | 42 | e := new(jx.Encoder) 43 | response.Encode(e) 44 | if _, err := e.WriteTo(w); err != nil { 45 | return errors.Wrap(err, "write") 46 | } 47 | 48 | return nil 49 | 50 | case *GetPetByIdNotFound: 51 | w.WriteHeader(404) 52 | span.SetStatus(codes.Error, http.StatusText(404)) 53 | 54 | return nil 55 | 56 | default: 57 | return errors.Errorf("unexpected response type: %T", response) 58 | } 59 | } 60 | 61 | func encodeUpdatePetResponse(response *UpdatePetOK, w http.ResponseWriter, span trace.Span) error { 62 | w.WriteHeader(200) 63 | span.SetStatus(codes.Ok, http.StatusText(200)) 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/oas/oas_router_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | 10 | "github.com/ogen-go/ogen/uri" 11 | ) 12 | 13 | func (s *Server) cutPrefix(path string) (string, bool) { 14 | prefix := s.cfg.Prefix 15 | if prefix == "" { 16 | return path, true 17 | } 18 | if !strings.HasPrefix(path, prefix) { 19 | // Prefix doesn't match. 20 | return "", false 21 | } 22 | // Cut prefix from the path. 23 | return strings.TrimPrefix(path, prefix), true 24 | } 25 | 26 | // ServeHTTP serves http request as defined by OpenAPI v3 specification, 27 | // calling handler that matches the path or returning not found error. 28 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 29 | elem := r.URL.Path 30 | elemIsEscaped := false 31 | if rawPath := r.URL.RawPath; rawPath != "" { 32 | if normalized, ok := uri.NormalizeEscapedPath(rawPath); ok { 33 | elem = normalized 34 | elemIsEscaped = strings.ContainsRune(elem, '%') 35 | } 36 | } 37 | 38 | elem, ok := s.cutPrefix(elem) 39 | if !ok || len(elem) == 0 { 40 | s.notFound(w, r) 41 | return 42 | } 43 | args := [1]string{} 44 | 45 | // Static code generated router with unwrapped path search. 46 | switch { 47 | default: 48 | if len(elem) == 0 { 49 | break 50 | } 51 | switch elem[0] { 52 | case '/': // Prefix: "/pet" 53 | 54 | if l := len("/pet"); len(elem) >= l && elem[0:l] == "/pet" { 55 | elem = elem[l:] 56 | } else { 57 | break 58 | } 59 | 60 | if len(elem) == 0 { 61 | switch r.Method { 62 | case "POST": 63 | s.handleAddPetRequest([0]string{}, elemIsEscaped, w, r) 64 | default: 65 | s.notAllowed(w, r, "POST") 66 | } 67 | 68 | return 69 | } 70 | switch elem[0] { 71 | case '/': // Prefix: "/" 72 | 73 | if l := len("/"); len(elem) >= l && elem[0:l] == "/" { 74 | elem = elem[l:] 75 | } else { 76 | break 77 | } 78 | 79 | // Param: "petId" 80 | // Leaf parameter, slashes are prohibited 81 | idx := strings.IndexByte(elem, '/') 82 | if idx >= 0 { 83 | break 84 | } 85 | args[0] = elem 86 | elem = "" 87 | 88 | if len(elem) == 0 { 89 | // Leaf node. 90 | switch r.Method { 91 | case "DELETE": 92 | s.handleDeletePetRequest([1]string{ 93 | args[0], 94 | }, elemIsEscaped, w, r) 95 | case "GET": 96 | s.handleGetPetByIdRequest([1]string{ 97 | args[0], 98 | }, elemIsEscaped, w, r) 99 | case "POST": 100 | s.handleUpdatePetRequest([1]string{ 101 | args[0], 102 | }, elemIsEscaped, w, r) 103 | default: 104 | s.notAllowed(w, r, "DELETE,GET,POST") 105 | } 106 | 107 | return 108 | } 109 | 110 | } 111 | 112 | } 113 | } 114 | s.notFound(w, r) 115 | } 116 | 117 | // Route is route object. 118 | type Route struct { 119 | name string 120 | summary string 121 | operationID string 122 | pathPattern string 123 | count int 124 | args [1]string 125 | } 126 | 127 | // Name returns ogen operation name. 128 | // 129 | // It is guaranteed to be unique and not empty. 130 | func (r Route) Name() string { 131 | return r.name 132 | } 133 | 134 | // Summary returns OpenAPI summary. 135 | func (r Route) Summary() string { 136 | return r.summary 137 | } 138 | 139 | // OperationID returns OpenAPI operationId. 140 | func (r Route) OperationID() string { 141 | return r.operationID 142 | } 143 | 144 | // PathPattern returns OpenAPI path. 145 | func (r Route) PathPattern() string { 146 | return r.pathPattern 147 | } 148 | 149 | // Args returns parsed arguments. 150 | func (r Route) Args() []string { 151 | return r.args[:r.count] 152 | } 153 | 154 | // FindRoute finds Route for given method and path. 155 | // 156 | // Note: this method does not unescape path or handle reserved characters in path properly. Use FindPath instead. 157 | func (s *Server) FindRoute(method, path string) (Route, bool) { 158 | return s.FindPath(method, &url.URL{Path: path}) 159 | } 160 | 161 | // FindPath finds Route for given method and URL. 162 | func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { 163 | var ( 164 | elem = u.Path 165 | args = r.args 166 | ) 167 | if rawPath := u.RawPath; rawPath != "" { 168 | if normalized, ok := uri.NormalizeEscapedPath(rawPath); ok { 169 | elem = normalized 170 | } 171 | defer func() { 172 | for i, arg := range r.args[:r.count] { 173 | if unescaped, err := url.PathUnescape(arg); err == nil { 174 | r.args[i] = unescaped 175 | } 176 | } 177 | }() 178 | } 179 | 180 | elem, ok := s.cutPrefix(elem) 181 | if !ok { 182 | return r, false 183 | } 184 | 185 | // Static code generated router with unwrapped path search. 186 | switch { 187 | default: 188 | if len(elem) == 0 { 189 | break 190 | } 191 | switch elem[0] { 192 | case '/': // Prefix: "/pet" 193 | 194 | if l := len("/pet"); len(elem) >= l && elem[0:l] == "/pet" { 195 | elem = elem[l:] 196 | } else { 197 | break 198 | } 199 | 200 | if len(elem) == 0 { 201 | switch method { 202 | case "POST": 203 | r.name = AddPetOperation 204 | r.summary = "Add a new pet to the store" 205 | r.operationID = "addPet" 206 | r.pathPattern = "/pet" 207 | r.args = args 208 | r.count = 0 209 | return r, true 210 | default: 211 | return 212 | } 213 | } 214 | switch elem[0] { 215 | case '/': // Prefix: "/" 216 | 217 | if l := len("/"); len(elem) >= l && elem[0:l] == "/" { 218 | elem = elem[l:] 219 | } else { 220 | break 221 | } 222 | 223 | // Param: "petId" 224 | // Leaf parameter, slashes are prohibited 225 | idx := strings.IndexByte(elem, '/') 226 | if idx >= 0 { 227 | break 228 | } 229 | args[0] = elem 230 | elem = "" 231 | 232 | if len(elem) == 0 { 233 | // Leaf node. 234 | switch method { 235 | case "DELETE": 236 | r.name = DeletePetOperation 237 | r.summary = "Deletes a pet" 238 | r.operationID = "deletePet" 239 | r.pathPattern = "/pet/{petId}" 240 | r.args = args 241 | r.count = 1 242 | return r, true 243 | case "GET": 244 | r.name = GetPetByIdOperation 245 | r.summary = "Find pet by ID" 246 | r.operationID = "getPetById" 247 | r.pathPattern = "/pet/{petId}" 248 | r.args = args 249 | r.count = 1 250 | return r, true 251 | case "POST": 252 | r.name = UpdatePetOperation 253 | r.summary = "Updates a pet in the store" 254 | r.operationID = "updatePet" 255 | r.pathPattern = "/pet/{petId}" 256 | r.args = args 257 | r.count = 1 258 | return r, true 259 | default: 260 | return 261 | } 262 | } 263 | 264 | } 265 | 266 | } 267 | } 268 | return r, false 269 | } 270 | -------------------------------------------------------------------------------- /internal/oas/oas_schemas_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "github.com/go-faster/errors" 7 | ) 8 | 9 | // DeletePetOK is response for DeletePet operation. 10 | type DeletePetOK struct{} 11 | 12 | // GetPetByIdNotFound is response for GetPetById operation. 13 | type GetPetByIdNotFound struct{} 14 | 15 | func (*GetPetByIdNotFound) getPetByIdRes() {} 16 | 17 | // NewOptInt64 returns new OptInt64 with value set to v. 18 | func NewOptInt64(v int64) OptInt64 { 19 | return OptInt64{ 20 | Value: v, 21 | Set: true, 22 | } 23 | } 24 | 25 | // OptInt64 is optional int64. 26 | type OptInt64 struct { 27 | Value int64 28 | Set bool 29 | } 30 | 31 | // IsSet returns true if OptInt64 was set. 32 | func (o OptInt64) IsSet() bool { return o.Set } 33 | 34 | // Reset unsets value. 35 | func (o *OptInt64) Reset() { 36 | var v int64 37 | o.Value = v 38 | o.Set = false 39 | } 40 | 41 | // SetTo sets value to v. 42 | func (o *OptInt64) SetTo(v int64) { 43 | o.Set = true 44 | o.Value = v 45 | } 46 | 47 | // Get returns value and boolean that denotes whether value was set. 48 | func (o OptInt64) Get() (v int64, ok bool) { 49 | if !o.Set { 50 | return v, false 51 | } 52 | return o.Value, true 53 | } 54 | 55 | // Or returns value if set, or given parameter if does not. 56 | func (o OptInt64) Or(d int64) int64 { 57 | if v, ok := o.Get(); ok { 58 | return v 59 | } 60 | return d 61 | } 62 | 63 | // NewOptPetStatus returns new OptPetStatus with value set to v. 64 | func NewOptPetStatus(v PetStatus) OptPetStatus { 65 | return OptPetStatus{ 66 | Value: v, 67 | Set: true, 68 | } 69 | } 70 | 71 | // OptPetStatus is optional PetStatus. 72 | type OptPetStatus struct { 73 | Value PetStatus 74 | Set bool 75 | } 76 | 77 | // IsSet returns true if OptPetStatus was set. 78 | func (o OptPetStatus) IsSet() bool { return o.Set } 79 | 80 | // Reset unsets value. 81 | func (o *OptPetStatus) Reset() { 82 | var v PetStatus 83 | o.Value = v 84 | o.Set = false 85 | } 86 | 87 | // SetTo sets value to v. 88 | func (o *OptPetStatus) SetTo(v PetStatus) { 89 | o.Set = true 90 | o.Value = v 91 | } 92 | 93 | // Get returns value and boolean that denotes whether value was set. 94 | func (o OptPetStatus) Get() (v PetStatus, ok bool) { 95 | if !o.Set { 96 | return v, false 97 | } 98 | return o.Value, true 99 | } 100 | 101 | // Or returns value if set, or given parameter if does not. 102 | func (o OptPetStatus) Or(d PetStatus) PetStatus { 103 | if v, ok := o.Get(); ok { 104 | return v 105 | } 106 | return d 107 | } 108 | 109 | // NewOptString returns new OptString with value set to v. 110 | func NewOptString(v string) OptString { 111 | return OptString{ 112 | Value: v, 113 | Set: true, 114 | } 115 | } 116 | 117 | // OptString is optional string. 118 | type OptString struct { 119 | Value string 120 | Set bool 121 | } 122 | 123 | // IsSet returns true if OptString was set. 124 | func (o OptString) IsSet() bool { return o.Set } 125 | 126 | // Reset unsets value. 127 | func (o *OptString) Reset() { 128 | var v string 129 | o.Value = v 130 | o.Set = false 131 | } 132 | 133 | // SetTo sets value to v. 134 | func (o *OptString) SetTo(v string) { 135 | o.Set = true 136 | o.Value = v 137 | } 138 | 139 | // Get returns value and boolean that denotes whether value was set. 140 | func (o OptString) Get() (v string, ok bool) { 141 | if !o.Set { 142 | return v, false 143 | } 144 | return o.Value, true 145 | } 146 | 147 | // Or returns value if set, or given parameter if does not. 148 | func (o OptString) Or(d string) string { 149 | if v, ok := o.Get(); ok { 150 | return v 151 | } 152 | return d 153 | } 154 | 155 | // Ref: #/components/schemas/Pet 156 | type Pet struct { 157 | ID OptInt64 `json:"id"` 158 | Name string `json:"name"` 159 | PhotoUrls []string `json:"photoUrls"` 160 | Status OptPetStatus `json:"status"` 161 | } 162 | 163 | // GetID returns the value of ID. 164 | func (s *Pet) GetID() OptInt64 { 165 | return s.ID 166 | } 167 | 168 | // GetName returns the value of Name. 169 | func (s *Pet) GetName() string { 170 | return s.Name 171 | } 172 | 173 | // GetPhotoUrls returns the value of PhotoUrls. 174 | func (s *Pet) GetPhotoUrls() []string { 175 | return s.PhotoUrls 176 | } 177 | 178 | // GetStatus returns the value of Status. 179 | func (s *Pet) GetStatus() OptPetStatus { 180 | return s.Status 181 | } 182 | 183 | // SetID sets the value of ID. 184 | func (s *Pet) SetID(val OptInt64) { 185 | s.ID = val 186 | } 187 | 188 | // SetName sets the value of Name. 189 | func (s *Pet) SetName(val string) { 190 | s.Name = val 191 | } 192 | 193 | // SetPhotoUrls sets the value of PhotoUrls. 194 | func (s *Pet) SetPhotoUrls(val []string) { 195 | s.PhotoUrls = val 196 | } 197 | 198 | // SetStatus sets the value of Status. 199 | func (s *Pet) SetStatus(val OptPetStatus) { 200 | s.Status = val 201 | } 202 | 203 | func (*Pet) getPetByIdRes() {} 204 | 205 | // Pet status in the store. 206 | // Ref: #/components/schemas/PetStatus 207 | type PetStatus string 208 | 209 | const ( 210 | PetStatusAvailable PetStatus = "available" 211 | PetStatusPending PetStatus = "pending" 212 | PetStatusSold PetStatus = "sold" 213 | ) 214 | 215 | // AllValues returns all PetStatus values. 216 | func (PetStatus) AllValues() []PetStatus { 217 | return []PetStatus{ 218 | PetStatusAvailable, 219 | PetStatusPending, 220 | PetStatusSold, 221 | } 222 | } 223 | 224 | // MarshalText implements encoding.TextMarshaler. 225 | func (s PetStatus) MarshalText() ([]byte, error) { 226 | switch s { 227 | case PetStatusAvailable: 228 | return []byte(s), nil 229 | case PetStatusPending: 230 | return []byte(s), nil 231 | case PetStatusSold: 232 | return []byte(s), nil 233 | default: 234 | return nil, errors.Errorf("invalid value: %q", s) 235 | } 236 | } 237 | 238 | // UnmarshalText implements encoding.TextUnmarshaler. 239 | func (s *PetStatus) UnmarshalText(data []byte) error { 240 | switch PetStatus(data) { 241 | case PetStatusAvailable: 242 | *s = PetStatusAvailable 243 | return nil 244 | case PetStatusPending: 245 | *s = PetStatusPending 246 | return nil 247 | case PetStatusSold: 248 | *s = PetStatusSold 249 | return nil 250 | default: 251 | return errors.Errorf("invalid value: %q", data) 252 | } 253 | } 254 | 255 | // UpdatePetOK is response for UpdatePet operation. 256 | type UpdatePetOK struct{} 257 | -------------------------------------------------------------------------------- /internal/oas/oas_server_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // Handler handles operations described by OpenAPI v3 specification. 10 | type Handler interface { 11 | // AddPet implements addPet operation. 12 | // 13 | // Add a new pet to the store. 14 | // 15 | // POST /pet 16 | AddPet(ctx context.Context, req *Pet) (*Pet, error) 17 | // DeletePet implements deletePet operation. 18 | // 19 | // Deletes a pet. 20 | // 21 | // DELETE /pet/{petId} 22 | DeletePet(ctx context.Context, params DeletePetParams) error 23 | // GetPetById implements getPetById operation. 24 | // 25 | // Returns a single pet. 26 | // 27 | // GET /pet/{petId} 28 | GetPetById(ctx context.Context, params GetPetByIdParams) (GetPetByIdRes, error) 29 | // UpdatePet implements updatePet operation. 30 | // 31 | // Updates a pet in the store. 32 | // 33 | // POST /pet/{petId} 34 | UpdatePet(ctx context.Context, params UpdatePetParams) error 35 | } 36 | 37 | // Server implements http server based on OpenAPI v3 specification and 38 | // calls Handler to handle requests. 39 | type Server struct { 40 | h Handler 41 | baseServer 42 | } 43 | 44 | // NewServer creates new Server. 45 | func NewServer(h Handler, opts ...ServerOption) (*Server, error) { 46 | s, err := newServerConfig(opts...).baseServer() 47 | if err != nil { 48 | return nil, err 49 | } 50 | return &Server{ 51 | h: h, 52 | baseServer: s, 53 | }, nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/oas/oas_unimplemented_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "context" 7 | 8 | ht "github.com/ogen-go/ogen/http" 9 | ) 10 | 11 | // UnimplementedHandler is no-op Handler which returns http.ErrNotImplemented. 12 | type UnimplementedHandler struct{} 13 | 14 | var _ Handler = UnimplementedHandler{} 15 | 16 | // AddPet implements addPet operation. 17 | // 18 | // Add a new pet to the store. 19 | // 20 | // POST /pet 21 | func (UnimplementedHandler) AddPet(ctx context.Context, req *Pet) (r *Pet, _ error) { 22 | return r, ht.ErrNotImplemented 23 | } 24 | 25 | // DeletePet implements deletePet operation. 26 | // 27 | // Deletes a pet. 28 | // 29 | // DELETE /pet/{petId} 30 | func (UnimplementedHandler) DeletePet(ctx context.Context, params DeletePetParams) error { 31 | return ht.ErrNotImplemented 32 | } 33 | 34 | // GetPetById implements getPetById operation. 35 | // 36 | // Returns a single pet. 37 | // 38 | // GET /pet/{petId} 39 | func (UnimplementedHandler) GetPetById(ctx context.Context, params GetPetByIdParams) (r GetPetByIdRes, _ error) { 40 | return r, ht.ErrNotImplemented 41 | } 42 | 43 | // UpdatePet implements updatePet operation. 44 | // 45 | // Updates a pet in the store. 46 | // 47 | // POST /pet/{petId} 48 | func (UnimplementedHandler) UpdatePet(ctx context.Context, params UpdatePetParams) error { 49 | return ht.ErrNotImplemented 50 | } 51 | -------------------------------------------------------------------------------- /internal/oas/oas_validators_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "github.com/go-faster/errors" 7 | 8 | "github.com/ogen-go/ogen/validate" 9 | ) 10 | 11 | func (s *Pet) Validate() error { 12 | if s == nil { 13 | return validate.ErrNilPointer 14 | } 15 | 16 | var failures []validate.FieldError 17 | if err := func() error { 18 | if value, ok := s.Status.Get(); ok { 19 | if err := func() error { 20 | if err := value.Validate(); err != nil { 21 | return err 22 | } 23 | return nil 24 | }(); err != nil { 25 | return err 26 | } 27 | } 28 | return nil 29 | }(); err != nil { 30 | failures = append(failures, validate.FieldError{ 31 | Name: "status", 32 | Error: err, 33 | }) 34 | } 35 | if len(failures) > 0 { 36 | return &validate.Error{Fields: failures} 37 | } 38 | return nil 39 | } 40 | 41 | func (s PetStatus) Validate() error { 42 | switch s { 43 | case "available": 44 | return nil 45 | case "pending": 46 | return nil 47 | case "sold": 48 | return nil 49 | default: 50 | return errors.Errorf("invalid value: %v", s) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest as builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | 8 | COPY . ./ 9 | RUN CGO_ENABLED=0 go build -o /app/api-server ./cmd/api-server 10 | 11 | FROM alpine:latest 12 | RUN apk --no-cache add ca-certificates 13 | 14 | WORKDIR /app 15 | COPY --from=builder /app/api-server /api-server 16 | 17 | ENTRYPOINT ["/api-server"] 18 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package example 4 | 5 | // Import to keep it in go.mod. 6 | import _ "github.com/ogen-go/ogen/cmd/ogen" 7 | --------------------------------------------------------------------------------