From 513aa7522da36642ff296092fce9dbb3f099682e Mon Sep 17 00:00:00 2001 From: Kuisong Tong Date: Sun, 28 Apr 2024 14:54:42 -0700 Subject: [PATCH] support http server/client (#26) --- .github/dependabot.yml | 7 + .github/workflows/coverage.yml | 2 +- .github/workflows/lint.yml | 6 +- .github/workflows/release.yml | 3 +- .github/workflows/test.yml | 28 +++- CHANGELOG.md | 1 + examples/grpc/go.mod | 2 +- examples/grpc/go.sum | 6 +- examples/http/cmd/config/config.yaml | 0 examples/http/cmd/config/prod.yaml | 0 examples/http/cmd/config/staging.yaml | 0 examples/http/cmd/main.go | 35 +++++ examples/http/go.mod | 58 ++++++++ examples/http/go.sum | 189 ++++++++++++++++++++++++++ grpc/server.go | 2 +- grpc/server_test.go | 36 ++--- http/client.go | 81 +++++++++++ http/client_test.go | 92 +++++++++++++ http/doc.go | 5 + http/go.mod | 11 ++ http/go.sum | 8 ++ http/internal/assert/assert.go | 36 +++++ http/internal/buffer.go | 54 ++++++++ http/internal/buffer_test.go | 153 +++++++++++++++++++++ http/internal/recovery.go | 38 ++++++ http/internal/recovery_test.go | 69 ++++++++++ http/internal/unix.go | 73 ++++++++++ http/internal/unix_test.go | 52 +++++++ http/option.go | 65 +++++++++ http/server.go | 161 ++++++++++++++++++++++ http/server_test.go | 139 +++++++++++++++++++ 31 files changed, 1376 insertions(+), 36 deletions(-) create mode 100644 examples/http/cmd/config/config.yaml create mode 100644 examples/http/cmd/config/prod.yaml create mode 100644 examples/http/cmd/config/staging.yaml create mode 100644 examples/http/cmd/main.go create mode 100644 examples/http/go.mod create mode 100644 examples/http/go.sum create mode 100644 http/client.go create mode 100644 http/client_test.go create mode 100644 http/doc.go create mode 100644 http/go.mod create mode 100644 http/go.sum create mode 100644 http/internal/assert/assert.go create mode 100644 http/internal/buffer.go create mode 100644 http/internal/buffer_test.go create mode 100644 http/internal/recovery.go create mode 100644 http/internal/recovery_test.go create mode 100644 http/internal/unix.go create mode 100644 http/internal/unix_test.go create mode 100644 http/option.go create mode 100644 http/server.go create mode 100644 http/server_test.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0db9412..b649ed6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -22,6 +22,13 @@ updates: schedule: interval: weekly + - package-ecosystem: gomod + directory: /http + labels: + - Skip-Changelog + schedule: + interval: weekly + - package-ecosystem: github-actions directory: / labels: diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index e7a2d75..f31dbf2 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -12,7 +12,7 @@ jobs: if: ${{ github.actor != 'dependabot[bot]' }} strategy: matrix: - module: [ '', 'gcp', 'grpc' ] + module: [ '', 'gcp', 'grpc', 'http' ] name: Coverage runs-on: ubuntu-latest steps: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 79d8e4f..6dfdbdf 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,7 +11,11 @@ jobs: lint: strategy: matrix: - module: [ '', 'gcp', 'grpc', 'examples/grpc' ] + module: [ + '', 'gcp', + 'grpc', 'examples/grpc', + 'http', 'examples/http' + ] name: Lint runs-on: ubuntu-latest steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b36b63b..a56999c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,7 +52,8 @@ jobs: script: | const modules = [ 'gcp', - 'grpc' + 'grpc', + 'http' ] for (const module of modules) { github.rest.git.createRef({ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a2f5298..4459c83 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,10 @@ jobs: test: strategy: matrix: - module: [ '', 'gcp', 'grpc', 'examples/grpc' ] + module: [ + '', 'gcp', + 'grpc', 'examples/grpc' + ] go-version: [ 'stable', 'oldstable' ] name: Test runs-on: ubuntu-latest @@ -27,11 +30,32 @@ jobs: - name: Test run: go test -shuffle=on -v ./... working-directory: ${{ matrix.module }} + test-http: + strategy: + matrix: + module: [ + 'http', 'examples/http' + ] + go-version: [ 'stable' ] + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache-dependency-path: "**/go.sum" + - name: Race Test + run: go test -v -shuffle=on -count=10 -race ./... + working-directory: ${{ matrix.module }} + - name: Test + run: go test -shuffle=on -v ./... + working-directory: ${{ matrix.module }} all: if: ${{ always() }} runs-on: ubuntu-latest name: All Tests - needs: test + needs: [test, test-http] steps: - name: Check test matrix status if: ${{ needs.test.result != 'success' }} diff --git a/CHANGELOG.md b/CHANGELOG.md index c6f532b..91c55eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,3 +13,4 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Support gRPC server (#15). - Add nilgo.Run (#22). - Support GCP Cloud Run (#24). +- Support HTTP server (#26). diff --git a/examples/grpc/go.mod b/examples/grpc/go.mod index 29002b6..64112b2 100644 --- a/examples/grpc/go.mod +++ b/examples/grpc/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( cloud.google.com/go/compute/metadata v0.3.0 - github.com/nil-go/nilgo v0.0.0-20240415132939-a595e188f79c + github.com/nil-go/nilgo v0.0.0 github.com/nil-go/nilgo/gcp v0.0.0 github.com/nil-go/nilgo/grpc v0.0.0 ) diff --git a/examples/grpc/go.sum b/examples/grpc/go.sum index 42ccfad..6fe3d6e 100644 --- a/examples/grpc/go.sum +++ b/examples/grpc/go.sum @@ -106,10 +106,8 @@ go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= -go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= -go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= -go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y= -go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/examples/http/cmd/config/config.yaml b/examples/http/cmd/config/config.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/http/cmd/config/prod.yaml b/examples/http/cmd/config/prod.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/http/cmd/config/staging.yaml b/examples/http/cmd/config/staging.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/http/cmd/main.go b/examples/http/cmd/main.go new file mode 100644 index 0000000..3e3fa8b --- /dev/null +++ b/examples/http/cmd/main.go @@ -0,0 +1,35 @@ +// Copyright (c) 2024 The nilgo authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package main + +import ( + "embed" + "net/http" + "time" + + "cloud.google.com/go/compute/metadata" + + "github.com/nil-go/nilgo" + "github.com/nil-go/nilgo/config" + "github.com/nil-go/nilgo/gcp" + nhttp "github.com/nil-go/nilgo/http" +) + +//go:embed config +var configFS embed.FS + +func main() { + var opts []any + if metadata.OnGCE() { + opts = gcp.Options() + } + opts = append(opts, + config.WithFS(configFS), + nhttp.Run(&http.Server{ReadTimeout: time.Second}), + ) + + if err := nilgo.Run(opts...); err != nil { + panic(err) + } +} diff --git a/examples/http/go.mod b/examples/http/go.mod new file mode 100644 index 0000000..dd50b8c --- /dev/null +++ b/examples/http/go.mod @@ -0,0 +1,58 @@ +module github.com/nil-go/nilgo/examples/http + +go 1.22 + +toolchain go1.22.2 + +require ( + cloud.google.com/go/compute/metadata v0.3.0 + github.com/nil-go/nilgo v0.0.0 + github.com/nil-go/nilgo/gcp v0.0.0 + github.com/nil-go/nilgo/http v0.0.0 +) + +replace ( + github.com/nil-go/nilgo => ./../.. + github.com/nil-go/nilgo/gcp => ./../../gcp + github.com/nil-go/nilgo/http => ./../../http +) + +require ( + cloud.google.com/go v0.112.2 // indirect + cloud.google.com/go/auth v0.3.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect + cloud.google.com/go/profiler v0.4.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/nil-go/konf v1.1.0 // indirect + github.com/nil-go/sloth v0.3.0 // indirect + github.com/nil-go/sloth/otel v0.3.0 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect + go.opentelemetry.io/otel v1.26.0 // indirect + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/otel/trace v1.26.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/oauth2 v0.19.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/api v0.176.1 // indirect + google.golang.org/genproto v0.0.0-20240415180920-8c6c420018be // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect + google.golang.org/grpc v1.63.2 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/examples/http/go.sum b/examples/http/go.sum new file mode 100644 index 0000000..efd6fdf --- /dev/null +++ b/examples/http/go.sum @@ -0,0 +1,189 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw= +cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= +cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs= +cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= +cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= +cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= +cloud.google.com/go/profiler v0.4.0 h1:ZeRDZbsOBDyRG0OiK0Op1/XWZ3xeLwJc9zjkzczUxyY= +cloud.google.com/go/profiler v0.4.0/go.mod h1:RvPlm4dilIr3oJtAOeFQU9Lrt5RoySHSDj4pTd6TWeU= +cloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY= +cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= +github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/nil-go/konf v1.1.0 h1:2rX5lC9B/oo31IuWOeb1Hd0M4UEx3WTm+hsQQPF392Y= +github.com/nil-go/konf v1.1.0/go.mod h1:ULW6PmJzWMd0F4KKNJQPhWD6Zu5eoX9U1C9SW2BPAr4= +github.com/nil-go/sloth v0.3.0 h1:lAqd8/pH6psoXZDpScCefY+3V9PVfJnIyOqMK1GSvwo= +github.com/nil-go/sloth v0.3.0/go.mod h1:SE8dLU9DLYeuLtu3kHp9PUEyj0OwUGKvTjSpx8tPdwo= +github.com/nil-go/sloth/otel v0.3.0 h1:BCF2oExOkzDjky8IHkT/4CqKvimbQldWp650qNk+vBo= +github.com/nil-go/sloth/otel v0.3.0/go.mod h1:AgNrkeeSag05r0HnTSotvYWmC8KF1XemF1NRN2iwYfk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= +golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.176.1 h1:DJSXnV6An+NhJ1J+GWtoF2nHEuqB1VNoTfnIbjNvwD4= +google.golang.org/api v0.176.1/go.mod h1:j2MaSDYcvYV1lkZ1+SMW4IeF90SrEyFA+tluDYWRrFg= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20240415180920-8c6c420018be h1:g4aX8SUFA8V5F4LrSY5EclyGYw1OZN4HS1jTyjB9ZDc= +google.golang.org/genproto v0.0.0-20240415180920-8c6c420018be/go.mod h1:FeSdT5fk+lkxatqJP38MsUicGqHax5cLtmy/6TAuxO4= +google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU= +google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/grpc/server.go b/grpc/server.go index eeceb77..d4e4cf8 100644 --- a/grpc/server.go +++ b/grpc/server.go @@ -24,7 +24,7 @@ import ( // NewServer creates a new gRPC server with the given options. // -// It wraps grpc.NewServer with built-in interceptors, e.g recovery, log buffering, and statsHandler. +// It wraps grpc.NewServer with built-in interceptors, e.g recovery, log buffering. func NewServer(opts ...grpc.ServerOption) *grpc.Server { handler := slog.Default().Handler() builtInOpts := []grpc.ServerOption{grpc.WaitForHandlers(true)} diff --git a/grpc/server_test.go b/grpc/server_test.go index c3f71de..a9c5d33 100644 --- a/grpc/server_test.go +++ b/grpc/server_test.go @@ -1,8 +1,6 @@ // Copyright (c) 2024 The nilgo authors // Use of this source code is governed by a MIT license found in the LICENSE file. -//go:build !race - package grpc_test import ( @@ -26,11 +24,13 @@ import ( ) func TestRun(t *testing.T) { + t.Parallel() + testcases := []struct { description string server func() *grpc.Server opts []ngrpc.Option - check func(conn *grpc.ClientConn) + assertion func(*grpc.ClientConn) }{ { description: "nil server", @@ -54,7 +54,7 @@ func TestRun(t *testing.T) { return server }, - check: func(conn *grpc.ClientConn) { + assertion: func(conn *grpc.ClientConn) { ctx := context.Background() client := grpc_testing.NewTestServiceClient(conn) @@ -67,7 +67,7 @@ func TestRun(t *testing.T) { description: "default config service", server: func() *grpc.Server { return ngrpc.NewServer() }, opts: []ngrpc.Option{ngrpc.WithConfigService()}, - check: func(conn *grpc.ClientConn) { + assertion: func(conn *grpc.ClientConn) { client := pb.NewConfigServiceClient(conn) resp, err := client.Explain(context.Background(), &pb.ExplainRequest{Path: "user"}) require.NoError(t, err) @@ -78,7 +78,7 @@ func TestRun(t *testing.T) { description: "config service", server: func() *grpc.Server { return ngrpc.NewServer() }, opts: []ngrpc.Option{ngrpc.WithConfigService(konf.New(), konf.New())}, - check: func(conn *grpc.ClientConn) { + assertion: func(conn *grpc.ClientConn) { client := pb.NewConfigServiceClient(conn) resp, err := client.Explain(context.Background(), &pb.ExplainRequest{Path: "user"}) require.NoError(t, err) @@ -93,35 +93,21 @@ func TestRun(t *testing.T) { return server }, - check: func(conn *grpc.ClientConn) { + assertion: func(conn *grpc.ClientConn) { client := grpc_testing.NewTestServiceClient(conn) resp, err := client.UnimplementedCall(context.Background(), &grpc_testing.Empty{}) require.EqualError(t, err, "rpc error: code = Internal desc = ") assert.Nil(t, resp) }, }, - { - description: "sampling handler", - server: func() *grpc.Server { - t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", "info") - - return ngrpc.NewServer() - }, - }, - { - description: "slog handler", - server: func() *grpc.Server { - t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", "info") - - return ngrpc.NewServer() - }, - }, } for _, testcase := range testcases { testcase := testcase t.Run(testcase.description, func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -148,8 +134,8 @@ func TestRun(t *testing.T) { require.NoError(t, err) require.NoError(t, stream.CloseSend()) - if testcase.check != nil { - testcase.check(conn) + if testcase.assertion != nil { + testcase.assertion(conn) } }) } diff --git a/http/client.go b/http/client.go new file mode 100644 index 0000000..efc9169 --- /dev/null +++ b/http/client.go @@ -0,0 +1,81 @@ +// Copyright (c) 2024 The nilgo authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package http + +import ( + "net" + "net/http" + "time" + + "github.com/nil-go/nilgo/http/internal" +) + +// NewClient creates a new http client with recommended production-ready settings. +func NewClient(opts ...ClientOption) *http.Client { + option := &clientOptions{} + for _, opt := range opts { + opt(option) + } + if option.timeout == 0 { + option.timeout = time.Second + } + if option.maxConnections == 0 { + option.maxConnections = 100 + } + + transportTimeout := option.timeout / 5 //nolint:gomnd + transport := &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: transportTimeout, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: int(option.maxConnections), + MaxIdleConnsPerHost: int(option.maxConnections), + TLSHandshakeTimeout: transportTimeout, + } + if option.unixSocket { + internal.RegisterUnixProtocol(transport) + } + + return &http.Client{ + Transport: transport, + Timeout: option.timeout, + } +} + +// WithClientTimeout provides the duration that timeout http request. +// +// By default, it has 1 second timeout. +func WithClientTimeout(timeout time.Duration) ClientOption { + return func(options *clientOptions) { + options.timeout = timeout + } +} + +// WithClientMaxConnections provides the maximum number of idle (keep-alive) connections +// across all hosts. +// +// By default, it has 100 idle connections. +func WithClientMaxConnections(maxConnections uint) ClientOption { + return func(options *clientOptions) { + options.maxConnections = maxConnections + } +} + +// WithClientUnixSocket enables the unix socket support for the http client. +func WithClientUnixSocket() ClientOption { + return func(options *clientOptions) { + options.unixSocket = true + } +} + +type ( + // ClientOption configures the http Client. + ClientOption func(*clientOptions) + clientOptions struct { + timeout time.Duration + maxConnections uint + unixSocket bool + } +) diff --git a/http/client_test.go b/http/client_test.go new file mode 100644 index 0000000..910ce79 --- /dev/null +++ b/http/client_test.go @@ -0,0 +1,92 @@ +// Copyright (c) 2024 The nilgo authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package http_test + +import ( + "context" + "net/http" + "testing" + "time" + + nhttp "github.com/nil-go/nilgo/http" + "github.com/nil-go/nilgo/http/internal/assert" +) + +func TestNewClient(t *testing.T) { + t.Parallel() + + testcases := []struct { + description string + opts []nhttp.ClientOption + assertion func(*http.Client) + }{ + { + description: "default", + assertion: func(client *http.Client) { + client.Timeout = time.Second + transport, ok := client.Transport.(*http.Transport) + assert.Equal(t, true, ok) + assert.Equal(t, 200*time.Millisecond, transport.TLSHandshakeTimeout) + assert.Equal(t, true, transport.ForceAttemptHTTP2) + assert.Equal(t, 100, transport.MaxIdleConns) + assert.Equal(t, 100, transport.MaxIdleConnsPerHost) + assert.Equal(t, 200*time.Millisecond, transport.TLSHandshakeTimeout) + + request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "unix://.test.sock", nil) + assert.NoError(t, err) + _, err = client.Do(request) //nolint:bodyclose + assert.EqualError(t, err, `Get "unix://.test.sock": unsupported protocol scheme "unix"`) + }, + }, + { + description: "with timeout", + opts: []nhttp.ClientOption{ + nhttp.WithClientTimeout(10 * time.Second), + }, + assertion: func(client *http.Client) { + client.Timeout = time.Second + transport, ok := client.Transport.(*http.Transport) + assert.Equal(t, true, ok) + assert.Equal(t, 2*time.Second, transport.TLSHandshakeTimeout) + assert.Equal(t, 2*time.Second, transport.TLSHandshakeTimeout) + }, + }, + { + description: "with max connections", + opts: []nhttp.ClientOption{ + nhttp.WithClientMaxConnections(10), + }, + assertion: func(client *http.Client) { + client.Timeout = time.Second + transport, ok := client.Transport.(*http.Transport) + assert.Equal(t, true, ok) + assert.Equal(t, 10, transport.MaxIdleConns) + assert.Equal(t, 10, transport.MaxIdleConnsPerHost) + }, + }, + { + description: "with unix socket", + opts: []nhttp.ClientOption{ + nhttp.WithClientUnixSocket(), + }, + assertion: func(client *http.Client) { + request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "unix://.test.sock", nil) + assert.NoError(t, err) + _, err = client.Do(request) //nolint:bodyclose + assert.EqualError(t, err, `Get "http://.test.sock": dial unix .test.sock: connect: no such file or directory`) + }, + }, + } + + for _, testcase := range testcases { + testcase := testcase + + t.Run(testcase.description, func(t *testing.T) { + t.Parallel() + + client := nhttp.NewClient(testcase.opts...) + testcase.assertion(client) + }) + } +} diff --git a/http/doc.go b/http/doc.go new file mode 100644 index 0000000..f2922c7 --- /dev/null +++ b/http/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) 2024 The nilgo authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +// Package http provides opinionated production-ready HTTP server and client. +package http diff --git a/http/go.mod b/http/go.mod new file mode 100644 index 0000000..904894c --- /dev/null +++ b/http/go.mod @@ -0,0 +1,11 @@ +module github.com/nil-go/nilgo/http + +go 1.22 + +require ( + github.com/nil-go/konf v1.1.0 + github.com/nil-go/sloth v0.3.0 + golang.org/x/net v0.24.0 +) + +require golang.org/x/text v0.14.0 // indirect diff --git a/http/go.sum b/http/go.sum new file mode 100644 index 0000000..e4db9bc --- /dev/null +++ b/http/go.sum @@ -0,0 +1,8 @@ +github.com/nil-go/konf v1.1.0 h1:2rX5lC9B/oo31IuWOeb1Hd0M4UEx3WTm+hsQQPF392Y= +github.com/nil-go/konf v1.1.0/go.mod h1:ULW6PmJzWMd0F4KKNJQPhWD6Zu5eoX9U1C9SW2BPAr4= +github.com/nil-go/sloth v0.3.0 h1:lAqd8/pH6psoXZDpScCefY+3V9PVfJnIyOqMK1GSvwo= +github.com/nil-go/sloth v0.3.0/go.mod h1:SE8dLU9DLYeuLtu3kHp9PUEyj0OwUGKvTjSpx8tPdwo= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/http/internal/assert/assert.go b/http/internal/assert/assert.go new file mode 100644 index 0000000..d6b6f6a --- /dev/null +++ b/http/internal/assert/assert.go @@ -0,0 +1,36 @@ +// Copyright (c) 2024 The nilgo authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package assert + +import ( + "reflect" + "testing" +) + +func Equal[T any](tb testing.TB, expected, actual T) { + tb.Helper() + + if !reflect.DeepEqual(expected, actual) { + tb.Errorf("\nexpected: %v\n actual: %v", expected, actual) + } +} + +func NoError(tb testing.TB, err error) { + tb.Helper() + + if err != nil { + tb.Errorf("unexpected error: %v", err) + } +} + +func EqualError(tb testing.TB, err error, message string) { + tb.Helper() + + switch { + case err == nil: + tb.Errorf("\n actual: \nexpected: %v", message) + case err.Error() != message: + tb.Errorf("\n actual: %v\nexpected: %v", err.Error(), message) + } +} diff --git a/http/internal/buffer.go b/http/internal/buffer.go new file mode 100644 index 0000000..e1fac32 --- /dev/null +++ b/http/internal/buffer.go @@ -0,0 +1,54 @@ +// Copyright (c) 2024 The nilgo authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package internal + +import ( + "log/slog" + "net/http" + "reflect" + "unsafe" + + "github.com/nil-go/sloth/sampling" +) + +// BufferInterceptor returns a http interceptor that inserts log buffer +// into context.Context for sampling.Handler. +func BufferInterceptor(handler http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + ctx, put := sampling.WithBuffer(request.Context()) + defer put() + + handler.ServeHTTP(writer, request.WithContext(ctx)) + }) +} + +func IsSamplingHandler(handler slog.Handler) bool { + switch handler.(type) { + case sampling.Handler, *sampling.Handler: + return true + default: + } + + value := reflect.ValueOf(handler) + if value.Kind() == reflect.Pointer { + value = value.Elem() + } + if value.Kind() != reflect.Struct { + return false + } + + // Check nested/embedded `slog.Handler`. + valueCopy := reflect.New(value.Type()).Elem() + valueCopy.Set(value) + for _, name := range []string{"handler", "Handler"} { + if v := valueCopy.FieldByName(name); v.IsValid() { + v = reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem() + if h, ok := v.Interface().(slog.Handler); ok { + return IsSamplingHandler(h) + } + } + } + + return false +} diff --git a/http/internal/buffer_test.go b/http/internal/buffer_test.go new file mode 100644 index 0000000..85a4156 --- /dev/null +++ b/http/internal/buffer_test.go @@ -0,0 +1,153 @@ +// Copyright (c) 2024 The nilgo authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package internal_test + +import ( + "bytes" + "context" + "log/slog" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/nil-go/sloth/sampling" + + "github.com/nil-go/nilgo/http/internal" + "github.com/nil-go/nilgo/http/internal/assert" +) + +func TestBufferInterceptor(t *testing.T) { + t.Parallel() + + testcases := []struct { + description string + records []slog.Record + expected string + }{ + { + description: "info only", + records: []slog.Record{ + slog.NewRecord(time.Time{}, slog.LevelInfo, "info", 0), + }, + }, + { + description: "with error", + records: []slog.Record{ + slog.NewRecord(time.Time{}, slog.LevelInfo, "info", 0), + slog.NewRecord(time.Time{}, slog.LevelError, "error", 0), + }, + expected: `level=INFO msg=info +level=ERROR msg=error +`, + }, + } + + for _, testcase := range testcases { + testcase := testcase + + t.Run(testcase.description, func(t *testing.T) { + t.Parallel() + + buf := &bytes.Buffer{} + logHandler := sampling.New( + slog.NewTextHandler(buf, &slog.HandlerOptions{}), + func(context.Context) bool { return false }, + ) + + handler := http.HandlerFunc(func(_ http.ResponseWriter, request *http.Request) { + for _, record := range testcase.records { + _ = logHandler.Handle(request.Context(), record) + } + }) + + writer := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/", nil) + internal.BufferInterceptor(handler).ServeHTTP(writer, request) + + pwd, _ := os.Getwd() + assert.Equal(t, testcase.expected, strings.ReplaceAll(buf.String(), pwd, "")) + }) + } +} + +func TestIsSamplingHandler(t *testing.T) { + t.Parallel() + + testcases := []struct { + description string + handler slog.Handler + expected bool + }{ + { + description: "nil handler", + }, + { + description: "sampling handler", + handler: sampling.Handler{}, + expected: true, + }, + { + description: "sampling handler (pointer)", + handler: &sampling.Handler{}, + expected: true, + }, + { + description: "embed sampling handler", + handler: handlerEmbed{sampling.Handler{}}, + expected: true, + }, + { + description: "embed sampling handler (pointer)", + handler: &handlerEmbed{sampling.Handler{}}, + expected: true, + }, + { + description: "nested sampling handler", + handler: handlerWrapper{handler: sampling.Handler{}}, + expected: true, + }, + { + description: "nested sampling handler (pointer)", + handler: &handlerWrapper{handler: sampling.Handler{}}, + expected: true, + }, + { + description: "deep nested sampling handler", + handler: handlerWrapper{handler: handlerWrapper{handler: sampling.Handler{}}}, + expected: true, + }, + { + description: "non sampling handler", + handler: slogDiscard{}, + }, + } + + for _, testcase := range testcases { + testcase := testcase + t.Run(testcase.description, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, testcase.expected, internal.IsSamplingHandler(testcase.handler)) + }) + } +} + +type handlerWrapper struct { + slogDiscard + handler slog.Handler +} + +type slogDiscard struct{} + +func (slogDiscard) Enabled(context.Context, slog.Level) bool { return false } +func (slogDiscard) Handle(context.Context, slog.Record) error { return nil } +func (slogDiscard) WithAttrs([]slog.Attr) slog.Handler { return slogDiscard{} } +func (slogDiscard) WithGroup(string) slog.Handler { return slogDiscard{} } + +type handlerEmbed struct { + slog.Handler +} diff --git a/http/internal/recovery.go b/http/internal/recovery.go new file mode 100644 index 0000000..a7ed105 --- /dev/null +++ b/http/internal/recovery.go @@ -0,0 +1,38 @@ +// Copyright (c) 2024 The nilgo authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package internal + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "runtime" + "time" +) + +// RecoveryInterceptor returns a server interceptor that recovers from HTTP handler panic. +// It overrides default recovery in [http] to provide better log integration. +func RecoveryInterceptor(handler http.Handler, logHandler slog.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + defer func(ctx context.Context) { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("%v", r) //nolint:goerr113 + } + + var pcs [1]uintptr + runtime.Callers(3, pcs[:]) //nolint:gomnd // Skip runtime.Callers, panic, this function. + record := slog.NewRecord(time.Now(), slog.LevelError, "Panic Recovered", pcs[0]) + record.AddAttrs(slog.Any("error", err)) + _ = logHandler.Handle(ctx, record) // Ignore error: It's fine to lose log. + + http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + }(request.Context()) + + handler.ServeHTTP(writer, request) + }) +} diff --git a/http/internal/recovery_test.go b/http/internal/recovery_test.go new file mode 100644 index 0000000..7c03384 --- /dev/null +++ b/http/internal/recovery_test.go @@ -0,0 +1,69 @@ +// Copyright (c) 2024 The nilgo authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package internal_test + +import ( + "bytes" + "log/slog" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/nil-go/nilgo/http/internal" + "github.com/nil-go/nilgo/http/internal/assert" +) + +func TestRecoveryInterceptor(t *testing.T) { + t.Parallel() + + testcases := []struct { + description string + handler func(http.ResponseWriter, *http.Request) + code int + log string + }{ + { + description: "normal", + handler: func(http.ResponseWriter, *http.Request) {}, + code: http.StatusOK, + }, + { + description: "panic", + handler: func(http.ResponseWriter, *http.Request) { panic("panic from handler") }, + code: http.StatusInternalServerError, + log: `level=ERROR source=/recovery_test.go:35 msg="Panic Recovered" error="panic from handler" +`, + }, + } + + for _, testcase := range testcases { + testcase := testcase + + t.Run(testcase.description, func(t *testing.T) { + t.Parallel() + + buf := &bytes.Buffer{} + handler := slog.NewTextHandler(buf, &slog.HandlerOptions{ + AddSource: true, + ReplaceAttr: func(groups []string, attr slog.Attr) slog.Attr { + if len(groups) == 0 && attr.Key == slog.TimeKey { + return slog.Attr{} + } + + return attr + }, + }) + + writer := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/", nil) + internal.RecoveryInterceptor(http.HandlerFunc(testcase.handler), handler).ServeHTTP(writer, request) + + assert.Equal(t, testcase.code, writer.Code) + pwd, _ := os.Getwd() + assert.Equal(t, testcase.log, strings.ReplaceAll(buf.String(), pwd, "")) + }) + } +} diff --git a/http/internal/unix.go b/http/internal/unix.go new file mode 100644 index 0000000..6efe7cd --- /dev/null +++ b/http/internal/unix.go @@ -0,0 +1,73 @@ +// Copyright (c) 2024 The nilgo authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package internal + +import ( + "context" + "net" + "net/http" + "strings" +) + +const unix = "unix" + +// RegisterUnixProtocol registers a protocol handler to the provided http.Transport +// that can server requests to Unix domain sockets via the "unix" schemes. +// +// Request URLs should have the following form: +// +// unix:socket_file/request/path?query=val&... +// +// The registered transport is based on a clone of the provided transport, +// and uses the same configuration: timeouts, TLS settings, and so on. +// Connection pooling should also work as expected. +func RegisterUnixProtocol(transport *http.Transport) { + defer func() { + // Ignore panic from RegisterProtocol while protocol is already registered. + _ = recover() + }() + + uTransport := newUnixTransport(transport.Clone()) + transport.RegisterProtocol(unix, uTransport) + transport.RegisterProtocol(unix+"s", uTransport) +} + +type unixTransport struct { + *http.Transport +} + +func newUnixTransport(transport *http.Transport) unixTransport { + dialContext := transport.DialContext + if dialContext == nil { + if t, ok := http.DefaultTransport.(*http.Transport); ok { + dialContext = t.DialContext + } + } + + transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + host = addr + } else { + network = unix + } + + return dialContext(ctx, network, host) + } + + return unixTransport{transport} +} + +func (u unixTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if strings.HasPrefix(req.URL.Scheme, unix) { + req.URL.Scheme = strings.Replace(req.URL.Scheme, "unix", "http", 1) + if req.URL.Host == "" && req.URL.Opaque != "" { + req.URL.Host, req.URL.Path, _ = strings.Cut(req.URL.Opaque, "/") + req.URL.Path = "/" + req.URL.Path + req.URL.Opaque = "" + } + } + + return u.Transport.RoundTrip(req) //nolint:wrapcheck +} diff --git a/http/internal/unix_test.go b/http/internal/unix_test.go new file mode 100644 index 0000000..62902d2 --- /dev/null +++ b/http/internal/unix_test.go @@ -0,0 +1,52 @@ +// Copyright (c) 2024 The nilgo authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package internal_test + +import ( + "context" + "crypto/rand" + "encoding/hex" + "net" + "net/http" + "os" + "testing" + "time" + + "github.com/nil-go/nilgo/http/internal" + "github.com/nil-go/nilgo/http/internal/assert" +) + +func TestRegisterUnixProtocol(t *testing.T) { + t.Parallel() + + randBytes := make([]byte, 4) //nolint:makezero + _, err := rand.Read(randBytes) + assert.NoError(t, err) + endpoint := "." + hex.EncodeToString(randBytes) + ".sock" + defer func() { + _ = os.Remove(endpoint) + }() + + go func() { + server := http.Server{ + Addr: endpoint, + Handler: http.HandlerFunc(func(_ http.ResponseWriter, request *http.Request) { + assert.Equal(t, "/", request.URL.Path) + }), + ReadTimeout: time.Second, + } + listener, serr := net.Listen("unix", endpoint) + assert.NoError(t, serr) + serr = server.Serve(listener) + assert.NoError(t, serr) + }() + time.Sleep(time.Second) // Wait for server to start. + + internal.RegisterUnixProtocol(http.DefaultTransport.(*http.Transport)) + request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "unix:"+endpoint+"/?query=val", nil) + assert.NoError(t, err) + resp, err := http.DefaultClient.Do(request) + defer func() { _ = resp.Body.Close() }() + assert.NoError(t, err) +} diff --git a/http/option.go b/http/option.go new file mode 100644 index 0000000..e432e03 --- /dev/null +++ b/http/option.go @@ -0,0 +1,65 @@ +// Copyright (c) 2024 The nilgo authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package http + +import ( + "slices" + "strings" + "time" + + "github.com/nil-go/konf" +) + +// WithAddress provides the address listened by the HTTP server. +// It should be either tcp address like `:8080` or unix socket address like `unix:nilgo.sock`. +// +// By default, it listens on `localhost:8080` or `:${PORT}` if the environment variable exists. +func WithAddress(addresses ...string) Option { + return func(options *options) { + options.addresses = slices.Grow(options.addresses, len(addresses)) + for _, address := range addresses { + network := "tcp" + if strings.HasPrefix(address, unix+":") { + network = unix + address = strings.TrimPrefix(address[5:], "//") + } + options.addresses = append(options.addresses, socket{network: network, address: address}) + } + } +} + +// WithTimeout provides the duration that timeout http request. +// +// By default, it has 10 seconds timeout. +func WithTimeout(timeout time.Duration) Option { + return func(options *options) { + options.timeout = timeout + } +} + +// WithConfigService registers the endpoint `_config/{path}` for config explanation. +// +// It uses the global konf.Config if the configs are not provided. +func WithConfigService(configs ...*konf.Config) Option { + return func(options *options) { + if options.configs == nil { + options.configs = []*konf.Config{} + } + options.configs = append(options.configs, configs...) + } +} + +type ( + // Option configures the http server. + Option func(*options) + options struct { + addresses []socket + timeout time.Duration + configs []*konf.Config + } + socket struct { + network string + address string + } +) diff --git a/http/server.go b/http/server.go new file mode 100644 index 0000000..5cd35e8 --- /dev/null +++ b/http/server.go @@ -0,0 +1,161 @@ +// Copyright (c) 2024 The nilgo authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package http + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net" + "net/http" + "os" + "slices" + "sync" + "time" + + "github.com/nil-go/konf" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + + "github.com/nil-go/nilgo/http/internal" +) + +const unix = "unix" + +// Run wraps start/stop of the HTTP/1 and HTTP/2 clear text server in a single run function +// with listening on multiple tcp and unix socket address. +// +// It also resister built-in interceptors, e.g recovery, log buffering, and timeout. +func Run(server *http.Server, opts ...Option) func(context.Context) error { //nolint:cyclop,funlen,gocognit + option := &options{} + for _, opt := range opts { + opt(option) + } + if option.timeout == 0 { + option.timeout = 10 * time.Second //nolint:gomnd + } + if server == nil { + server = &http.Server{ + ReadTimeout: option.timeout, + } + } + if server.ReadTimeout == 0 { + // It has to be longer than the timeout of the handler. + server.ReadTimeout = option.timeout * 2 //nolint:gomnd + } + if server.WriteTimeout == 0 { + server.WriteTimeout = server.ReadTimeout + } + if server.IdleTimeout == 0 { + server.IdleTimeout = server.ReadTimeout * 3 //nolint:gomnd + } + + if len(option.addresses) == 0 { + address := "localhost:8080" + if a := os.Getenv("PORT"); a != "" { + address = ":" + a + } + option.addresses = []socket{{network: "tcp", address: address}} + } + + handler := server.Handler + if handler == nil { + handler = http.DefaultServeMux + } + if option.configs != nil { + mux := http.NewServeMux() + mux.Handle("/", handler) + mux.HandleFunc("GET /_config/{path}", config(option)) + handler = mux + } + logHandler := slog.Default().Handler() + handler = internal.RecoveryInterceptor(handler, logHandler) + if internal.IsSamplingHandler(logHandler) { + handler = internal.BufferInterceptor(handler) + } + handler = http.TimeoutHandler(handler, option.timeout, "request timeout") + server.Handler = h2c.NewHandler(handler, &http2.Server{}) + + return func(ctx context.Context) error { + ctx, cancel := context.WithCancelCause(ctx) + defer cancel(nil) + + var waitGroup sync.WaitGroup + waitGroup.Add(len(option.addresses) + 1) + if slices.ContainsFunc(option.addresses, func(addr socket) bool { return addr.network == unix }) { + if transport, ok := http.DefaultTransport.(*http.Transport); ok { + internal.RegisterUnixProtocol(transport) + } + } + for _, addr := range option.addresses { + addr := addr + go func() { + defer waitGroup.Done() + + if addr.network == unix { + if err := os.RemoveAll(addr.address); err != nil { + slog.LogAttrs(ctx, slog.LevelWarn, "Could not delete unix socket file.", slog.Any("error", err)) + } + } + listener, err := net.Listen(addr.network, addr.address) + if err != nil { + cancel(fmt.Errorf("start listener: %w", err)) + + return + } + + slog.LogAttrs(ctx, slog.LevelInfo, fmt.Sprintf("HTTP Server listens on %s.", listener.Addr())) + if err := server.Serve(listener); err != nil { + cancel(fmt.Errorf("start HTTP Server on %s: %w", listener.Addr(), err)) + } + }() + } + go func() { + defer waitGroup.Done() + + <-ctx.Done() + if err := server.Shutdown(context.WithoutCancel(ctx)); err != nil { + cancel(fmt.Errorf("shutdown HTTP Server: %w", err)) + } + slog.LogAttrs(ctx, slog.LevelInfo, "HTTP Server is stopped.") + }() + waitGroup.Wait() + + if err := context.Cause(ctx); err != nil && !errors.Is(err, ctx.Err()) { + return err //nolint:wrapcheck + } + + return nil + } +} + +func config(option *options) func(write http.ResponseWriter, request *http.Request) { + return func(write http.ResponseWriter, request *http.Request) { + var err error + defer func() { + if err != nil { + http.Error(write, err.Error(), http.StatusInternalServerError) + } + }() + + path := request.PathValue("path") + if len(option.configs) == 0 { + _, err = write.Write([]byte(konf.Explain(path))) + + return + } + + for i, config := range option.configs { + if i > 0 { + if _, err := write.Write([]byte("\n-----\n")); err != nil { + return + } + } + if _, err := write.Write([]byte(config.Explain(path))); err != nil { + return + } + } + } +} diff --git a/http/server_test.go b/http/server_test.go new file mode 100644 index 0000000..be0c9b9 --- /dev/null +++ b/http/server_test.go @@ -0,0 +1,139 @@ +// Copyright (c) 2024 The nilgo authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package http_test + +import ( + "context" + "crypto/rand" + "encoding/hex" + "io" + "net/http" + "os" + "testing" + "time" + + nhttp "github.com/nil-go/nilgo/http" + "github.com/nil-go/nilgo/http/internal/assert" +) + +//nolint:gosec +func TestRun(t *testing.T) { + t.Parallel() + + testcases := []struct { + description string + server func() *http.Server + opts []nhttp.Option + assertion func(string) + }{ + { + description: "nil server", + server: func() *http.Server { return nil }, + assertion: func(endpoint string) { + request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, endpoint, nil) + assert.NoError(t, err) + resp, err := http.DefaultClient.Do(request) + assert.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + }, + }, + { + description: "organic server", + server: func() *http.Server { + return &http.Server{ + Handler: http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + panic("test") + }), + } + }, + assertion: func(endpoint string) { + request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, endpoint, nil) + assert.NoError(t, err) + resp, err := http.DefaultClient.Do(request) + assert.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + bytes, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + assert.Equal(t, "Internal Server Error\n", string(bytes)) + }, + }, + { + description: "with timeout", + server: func() *http.Server { + return &http.Server{ + Handler: http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + timer := time.NewTimer(2 * time.Second) + defer timer.Stop() + + select { + case <-r.Context().Done(): + case <-timer.C: + } + }), + } + }, + opts: []nhttp.Option{ + nhttp.WithTimeout(time.Second), + }, + assertion: func(endpoint string) { + request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, endpoint, nil) + assert.NoError(t, err) + resp, err := http.DefaultClient.Do(request) + assert.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) + }, + }, + { + description: "with config service", + server: func() *http.Server { + return &http.Server{} + }, + opts: []nhttp.Option{ + nhttp.WithConfigService(), + }, + assertion: func(endpoint string) { + request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, endpoint+"/_config/_not_found", nil) + assert.NoError(t, err) + resp, err := http.DefaultClient.Do(request) + assert.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + assert.Equal(t, http.StatusOK, resp.StatusCode) + bytes, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, "_not_found has no configuration.\n\n", string(bytes)) + }, + }, + } + + for _, testcase := range testcases { + testcase := testcase + + t.Run(testcase.description, func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + randBytes := make([]byte, 4) //nolint:makezero + _, err := rand.Read(randBytes) + assert.NoError(t, err) + endpoint := "." + hex.EncodeToString(randBytes) + ".sock" + defer func() { + _ = os.Remove(endpoint) + }() + go func() { + err := nhttp.Run(testcase.server(), append(testcase.opts, nhttp.WithAddress("unix:"+endpoint))...)(ctx) + assert.NoError(t, err) + }() + time.Sleep(100 * time.Millisecond) // Wait for server to start. + + if testcase.assertion != nil { + testcase.assertion("unix:" + endpoint) + } + }) + } +}