Skip to content

Commit 9ee029c

Browse files
authored
feat(grpcserver): Provided module (#87)
1 parent 717b4ab commit 9ee029c

31 files changed

+3515
-1
lines changed

.github/workflows/common-ci.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ jobs:
3535
run: go mod download
3636
- name: Run tests
3737
working-directory: ${{ inputs.module }}
38-
run: go test -v -race -failfast -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt ./...
38+
run: go test -v -race -failfast -coverpkg=./... -covermode=atomic -coverprofile=coverage.tmp.out ./...
39+
- name: Exclude testdata
40+
working-directory: ${{ inputs.module }}
41+
run: cat coverage.tmp.out | grep -v "testdata" > coverage.out
42+
- name: Clean temp coverage
43+
working-directory: ${{ inputs.module }}
44+
run: rm coverage.tmp.out
3945
- name: Codecov
4046
uses: codecov/codecov-action@v3
4147
with:

.github/workflows/coverage.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jobs:
2323
module:
2424
- "config"
2525
- "generate"
26+
- "grpcserver"
2627
- "healthcheck"
2728
- "httpclient"
2829
- "httpserver"

.github/workflows/grpcserver-ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: "grpcserver-ci"
2+
3+
on:
4+
push:
5+
branches:
6+
- "feat**"
7+
- "fix**"
8+
- "hotfix**"
9+
- "chore**"
10+
paths:
11+
- "grpcserver/**.go"
12+
- "grpcserver/go.mod"
13+
- "grpcserver/go.sum"
14+
pull_request:
15+
types:
16+
- opened
17+
- synchronize
18+
- reopened
19+
branches:
20+
- main
21+
paths:
22+
- "grpcserver/**.go"
23+
- "grpcserver/go.mod"
24+
- "grpcserver/go.sum"
25+
26+
jobs:
27+
ci:
28+
uses: ./.github/workflows/common-ci.yml
29+
secrets: inherit
30+
with:
31+
module: "grpcserver"

grpcserver/.golangci.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
run:
2+
timeout: 5m
3+
concurrency: 8
4+
5+
linters:
6+
enable:
7+
- asasalint
8+
- asciicheck
9+
- bidichk
10+
- bodyclose
11+
- containedctx
12+
- contextcheck
13+
- cyclop
14+
- decorder
15+
- dogsled
16+
- dupl
17+
- durationcheck
18+
- errcheck
19+
- errchkjson
20+
- errname
21+
- errorlint
22+
- exhaustive
23+
- forbidigo
24+
- forcetypeassert
25+
- gocognit
26+
- goconst
27+
- gocritic
28+
- gocyclo
29+
- godot
30+
- godox
31+
- gofmt
32+
- goheader
33+
- gomoddirectives
34+
- gomodguard
35+
- goprintffuncname
36+
- gosec
37+
- gosimple
38+
- govet
39+
- grouper
40+
- importas
41+
- ineffassign
42+
- interfacebloat
43+
- logrlint
44+
- maintidx
45+
- makezero
46+
- misspell
47+
- nestif
48+
- nilerr
49+
- nilnil
50+
- nlreturn
51+
- nolintlint
52+
- nosprintfhostport
53+
- prealloc
54+
- predeclared
55+
- promlinter
56+
- reassign
57+
- staticcheck
58+
- tenv
59+
- thelper
60+
- tparallel
61+
- typecheck
62+
- unconvert
63+
- unparam
64+
- unused
65+
- usestdlibvars
66+
- whitespace

grpcserver/README.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# gRPC Server Module
2+
3+
[![ci](https://github.com/ankorstore/yokai/actions/workflows/grpcserver-ci.yml/badge.svg)](https://github.com/ankorstore/yokai/actions/workflows/grpcserver-ci.yml)
4+
[![go report](https://goreportcard.com/badge/github.com/ankorstore/yokai/grpcserver)](https://goreportcard.com/report/github.com/ankorstore/yokai/grpcserver)
5+
[![codecov](https://codecov.io/gh/ankorstore/yokai/graph/badge.svg?token=ghUBlFsjhR&flag=grpcserver)](https://app.codecov.io/gh/ankorstore/yokai/tree/main/grpcserver)
6+
[![Deps](https://img.shields.io/badge/osi-deps-blue)](https://deps.dev/go/github.com%2Fankorstore%2Fyokai%2Fgrpcserver)
7+
[![PkgGoDev](https://pkg.go.dev/badge/github.com/ankorstore/yokai/grpcserver)](https://pkg.go.dev/github.com/ankorstore/yokai/grpcserver)
8+
9+
> gRPC server module based on [gRPC-Go](https://github.com/grpc/grpc-go).
10+
11+
<!-- TOC -->
12+
13+
* [Installation](#installation)
14+
* [Documentation](#documentation)
15+
* [Usage](#usage)
16+
* [Add-ons](#add-ons)
17+
* [Reflection](#reflection)
18+
* [Panic recovery](#panic-recovery)
19+
* [Logger interceptor](#logger-interceptor)
20+
* [Healthcheck service](#healthcheck-service)
21+
22+
<!-- TOC -->
23+
24+
## Installation
25+
26+
```shell
27+
go get github.com/ankorstore/yokai/grpcserver
28+
```
29+
30+
## Documentation
31+
32+
### Usage
33+
34+
This module provides a [GrpcServerFactory](factory.go), allowing to build an `grpc.Server` instance.
35+
36+
```go
37+
package main
38+
39+
import (
40+
"github.com/ankorstore/yokai/grpcserver"
41+
"google.golang.org/grpc"
42+
)
43+
44+
var server, _ = grpcserver.NewDefaultGrpcServerFactory().Create()
45+
46+
// equivalent to:
47+
var server, _ = grpcserver.NewDefaultGrpcServerFactory().Create(
48+
grpcserver.WithServerOptions([]grpc.ServerOption{}), // no specific server options by default
49+
grpcserver.WithReflection(false), // reflection disabled by default
50+
)
51+
```
52+
53+
See [gRPC-Go documentation](https://github.com/grpc/grpc-go) for more details.
54+
55+
### Add-ons
56+
57+
This module provides several add-ons ready to use to enrich your gRPC server.
58+
59+
#### Reflection
60+
61+
This module provides the possibility to easily
62+
enable [gRPC server reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md), disabled by default:
63+
64+
```go
65+
package main
66+
67+
import (
68+
"github.com/ankorstore/yokai/grpcserver"
69+
)
70+
71+
func main() {
72+
server, _ := grpcserver.NewDefaultGrpcServerFactory().Create(
73+
grpcserver.WithReflection(true),
74+
)
75+
}
76+
```
77+
78+
Reflection usage is helpful for developing or testing your gRPC services, but it is not recommended for production
79+
usage.
80+
81+
#### Panic recovery
82+
83+
This module provides an [GrpcPanicRecoveryHandler](panic.go), compatible with
84+
the [recovery interceptor](https://github.com/grpc-ecosystem/go-grpc-middleware/tree/main/interceptors/recovery), to
85+
automatically recover from panics in your gRPC services:
86+
87+
```go
88+
package main
89+
90+
import (
91+
"github.com/ankorstore/yokai/grpcserver"
92+
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery"
93+
"google.golang.org/grpc"
94+
)
95+
96+
func main() {
97+
handler := grpcserver.NewGrpcPanicRecoveryHandler()
98+
99+
server, _ := grpcserver.NewDefaultGrpcServerFactory().Create(
100+
grpcserver.WithServerOptions(
101+
grpc.UnaryInterceptor(recovery.UnaryServerInterceptor(recovery.WithRecoveryHandlerContext(handler.Handle(false)))),
102+
grpc.StreamInterceptor(recovery.StreamServerInterceptor(recovery.WithRecoveryHandlerContext(handler.Handle(false)))),
103+
),
104+
)
105+
}
106+
```
107+
108+
You can also use `Handle(true)` to append on the handler gRPC response and logs more information about the panic and the debug stack (
109+
not suitable for production).
110+
111+
#### Logger interceptor
112+
113+
This module provides a [GrpcLoggerInterceptor](logger.go) to automatically log unary and streaming RPCs calls (status,
114+
type, duration, etc.), compatible with the [log module](https://github.com/ankorstore/yokai/tree/main/log).
115+
116+
Using this interceptor will also provide a logger instance in the context, that you can retrieve with
117+
the [CtxLogger](context.go) method to produce correlated logs from your gRPC services.
118+
119+
```go
120+
package main
121+
122+
import (
123+
"github.com/ankorstore/yokai/generate/uuid"
124+
"github.com/ankorstore/yokai/grpcserver"
125+
"github.com/ankorstore/yokai/log"
126+
"google.golang.org/grpc"
127+
)
128+
129+
func main() {
130+
logger, _ := log.NewDefaultLoggerFactory().Create()
131+
132+
loggerInterceptor := grpcserver.NewGrpcLoggerInterceptor(uuid.NewDefaultUuidGenerator, logger)
133+
134+
server, _ := grpcserver.NewDefaultGrpcServerFactory().Create(
135+
grpcserver.WithServerOptions(
136+
grpc.UnaryInterceptor(loggerInterceptor.UnaryInterceptor()),
137+
grpc.StreamInterceptor(loggerInterceptor.StreamInterceptor()),
138+
),
139+
)
140+
}
141+
```
142+
143+
The interceptor will automatically enrich each log records with the `x-request-id` fetch from the context metadata in
144+
the field `requestID`.
145+
146+
You can specify additional metadata to add to logs records:
147+
148+
- the key is the metadata name to fetch
149+
- the value is the log field to fill
150+
151+
```go
152+
loggerInterceptor.Metadata(
153+
map[string]string{
154+
"x-some-metadata": "someMetadata",
155+
"x-other-metadata": "otherMetadata",
156+
},
157+
)
158+
```
159+
160+
You can also specify a list of gRPC methods to exclude from logging:
161+
162+
```go
163+
loggerInterceptor.Exlude("/test.Service/Unary", "/test.Service/Bidi")
164+
```
165+
166+
Note: even if excluded, failing gRPC methods calls will still be logged for observability purposes.
167+
168+
#### Healthcheck service
169+
170+
This module provides a [GrpcHealthCheckService](healthcheck.go), compatible with
171+
the [healthcheck module](https://github.com/ankorstore/yokai/tree/main/healthcheck):
172+
173+
```go
174+
package main
175+
176+
import (
177+
"probes"
178+
179+
"github.com/ankorstore/yokai/grpcserver"
180+
"github.com/ankorstore/yokai/healthcheck"
181+
"google.golang.org/grpc/health/grpc_health_v1"
182+
)
183+
184+
func main() {
185+
checker, _ := healthcheck.NewDefaultCheckerFactory().Create(
186+
healthcheck.WithProbe(probes.SomeProbe()), // register for startup, liveness and readiness
187+
healthcheck.WithProbe(probes.SomeOtherProbe(), healthcheck.Liveness), // register for liveness only
188+
)
189+
190+
server, _ := grpcserver.NewDefaultGrpcServerFactory().Create()
191+
192+
grpc_health_v1.RegisterHealthServer(server, grpcserver.NewGrpcHealthCheckService(checker))
193+
}
194+
```
195+
196+
This will expose the [Check and Watch](https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto) RPCs, suitable for [k8s startup, readiness or liveness probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/).
197+
198+
The checker will:
199+
200+
- run the `liveness` probes checks if the request service name contains `liveness` (like `kubernetes::liveness`)
201+
- or run the `readiness` probes checks if the request service name contains `readiness` (like `kubernetes::readiness`)
202+
- or run the `startup` probes checks otherwise

grpcserver/context.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package grpcserver
2+
3+
import (
4+
"context"
5+
6+
"github.com/ankorstore/yokai/log"
7+
"github.com/ankorstore/yokai/trace"
8+
oteltrace "go.opentelemetry.io/otel/trace"
9+
)
10+
11+
// TracerName is the grpcserver tracer name.
12+
const TracerName = "grpcserver"
13+
14+
// CtxLogger returns the contextual [log.Logger].
15+
func CtxLogger(ctx context.Context) *log.Logger {
16+
return log.CtxLogger(ctx)
17+
}
18+
19+
// CtxTracer returns the contextual [oteltrace.Tracer].
20+
func CtxTracer(ctx context.Context) oteltrace.Tracer {
21+
return trace.CtxTracerProvider(ctx).Tracer(TracerName)
22+
}

0 commit comments

Comments
 (0)