Initial QSfera import
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
mgrpcc "github.com/go-micro/plugins/v4/client/grpc"
|
||||
mtracer "github.com/go-micro/plugins/v4/wrapper/trace/opentelemetry"
|
||||
"github.com/qsfera/server/pkg/registry"
|
||||
"github.com/qsfera/server/pkg/shared"
|
||||
"go-micro.dev/v4/client"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.opentelemetry.io/otel/trace/noop"
|
||||
)
|
||||
|
||||
// ClientOptions represent options (e.g. tls settings) for the grpc clients
|
||||
type ClientOptions struct {
|
||||
tlsMode string
|
||||
caCert string
|
||||
tp trace.TracerProvider
|
||||
}
|
||||
|
||||
// Option is used to pass client options
|
||||
type ClientOption func(opts *ClientOptions)
|
||||
|
||||
// WithTLSMode allows setting the TLSMode option for grpc clients
|
||||
func WithTLSMode(v string) ClientOption {
|
||||
return func(o *ClientOptions) {
|
||||
o.tlsMode = v
|
||||
}
|
||||
}
|
||||
|
||||
// WithTLSCACert allows setting the CA Certificate for grpc clients
|
||||
func WithTLSCACert(v string) ClientOption {
|
||||
return func(o *ClientOptions) {
|
||||
o.caCert = v
|
||||
}
|
||||
}
|
||||
|
||||
// WithTraceProvider allows to set the trace Provider for grpc clients
|
||||
func WithTraceProvider(tp trace.TracerProvider) ClientOption {
|
||||
return func(o *ClientOptions) {
|
||||
if tp != nil {
|
||||
o.tp = tp
|
||||
} else {
|
||||
o.tp = noop.NewTracerProvider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetClientOptions(t *shared.GRPCClientTLS) []ClientOption {
|
||||
opts := []ClientOption{
|
||||
WithTLSMode(t.Mode),
|
||||
WithTLSCACert(t.CACert),
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func NewClient(opts ...ClientOption) (client.Client, error) {
|
||||
var options ClientOptions
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
reg := registry.GetRegistry()
|
||||
var tlsConfig *tls.Config
|
||||
cOpts := []client.Option{
|
||||
client.Registry(reg),
|
||||
client.Wrap(mtracer.NewClientWrapper(
|
||||
mtracer.WithTraceProvider(options.tp),
|
||||
)),
|
||||
}
|
||||
switch options.tlsMode {
|
||||
case "insecure":
|
||||
tlsConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
cOpts = append(cOpts, mgrpcc.AuthTLS(tlsConfig))
|
||||
case "on":
|
||||
tlsConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
// Note: If caCert is empty we use the system's default set of trusted CAs
|
||||
if options.caCert != "" {
|
||||
certs := x509.NewCertPool()
|
||||
pemData, err := os.ReadFile(options.caCert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !certs.AppendCertsFromPEM(pemData) {
|
||||
return nil, errors.New("could not initialize client, adding CA cert failed")
|
||||
}
|
||||
tlsConfig.RootCAs = certs
|
||||
}
|
||||
cOpts = append(cOpts, mgrpcc.AuthTLS(tlsConfig))
|
||||
// case "off":
|
||||
// default:
|
||||
}
|
||||
|
||||
return mgrpcc.NewClient(cOpts...), nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package ratelimiter
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/errors"
|
||||
"go-micro.dev/v4/server"
|
||||
)
|
||||
|
||||
// NewHandlerWrapper creates a blocking server side rate limiter.
|
||||
func NewHandlerWrapper(limit int) server.HandlerWrapper {
|
||||
if limit <= 0 {
|
||||
return func(h server.HandlerFunc) server.HandlerFunc {
|
||||
return h
|
||||
}
|
||||
}
|
||||
|
||||
token := make(chan struct{}, limit)
|
||||
for i := 0; i < limit; i++ {
|
||||
token <- struct{}{}
|
||||
}
|
||||
|
||||
return func(h server.HandlerFunc) server.HandlerFunc {
|
||||
return func(ctx context.Context, req server.Request, rsp any) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case t := <-token:
|
||||
defer func() {
|
||||
token <- t
|
||||
}()
|
||||
return h(ctx, req, rsp)
|
||||
default:
|
||||
return errors.New("go.micro.server", "Rate limit exceeded", 429)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
_serverMaxConnectionAgeEnv = "GRPC_MAX_CONNECTION_AGE"
|
||||
|
||||
// same default as grpc
|
||||
infinity = time.Duration(math.MaxInt64)
|
||||
_defaultMaxConnectionAge = infinity
|
||||
)
|
||||
|
||||
// GetMaxConnectionAge returns the maximum grpc connection age.
|
||||
func GetMaxConnectionAge() time.Duration {
|
||||
d, err := time.ParseDuration(os.Getenv(_serverMaxConnectionAgeEnv))
|
||||
if err != nil {
|
||||
return _defaultMaxConnectionAge
|
||||
}
|
||||
return d
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"go-micro.dev/v4/server"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Namespace string
|
||||
Name string
|
||||
Version string
|
||||
Address string
|
||||
TLSEnabled bool
|
||||
TLSCert string
|
||||
TLSKey string
|
||||
Context context.Context
|
||||
TraceProvider trace.TracerProvider
|
||||
HandlerWrappers []server.HandlerWrapper
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{
|
||||
Namespace: "go.micro.api",
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(l log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace provides a function to set the namespace option.
|
||||
func Namespace(n string) Option {
|
||||
return func(o *Options) {
|
||||
o.Namespace = n
|
||||
}
|
||||
}
|
||||
|
||||
// Name provides a function to set the name option.
|
||||
func Name(n string) Option {
|
||||
return func(o *Options) {
|
||||
o.Name = n
|
||||
}
|
||||
}
|
||||
|
||||
// Version provides a function to set the version option.
|
||||
func Version(v string) Option {
|
||||
return func(o *Options) {
|
||||
o.Version = v
|
||||
}
|
||||
}
|
||||
|
||||
// Address provides a function to set the address option.
|
||||
func Address(a string) Option {
|
||||
return func(o *Options) {
|
||||
o.Address = a
|
||||
}
|
||||
}
|
||||
|
||||
// TLSEnabled provides a function to enable/disable TLS
|
||||
func TLSEnabled(v bool) Option {
|
||||
return func(o *Options) {
|
||||
o.TLSEnabled = v
|
||||
}
|
||||
}
|
||||
|
||||
// TLSCert provides a function to set the TLS server certificate and key
|
||||
func TLSCert(c string, k string) Option {
|
||||
return func(o *Options) {
|
||||
o.TLSCert = c
|
||||
o.TLSKey = k
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(ctx context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// TraceProvider provides a function to set the trace provider option.
|
||||
func TraceProvider(tp trace.TracerProvider) Option {
|
||||
return func(o *Options) {
|
||||
o.TraceProvider = tp
|
||||
}
|
||||
}
|
||||
|
||||
func HandlerWrappers(w ...server.HandlerWrapper) Option {
|
||||
return func(o *Options) {
|
||||
o.HandlerWrappers = w
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mgrpcs "github.com/go-micro/plugins/v4/server/grpc"
|
||||
"github.com/go-micro/plugins/v4/wrapper/monitoring/prometheus"
|
||||
mtracer "github.com/go-micro/plugins/v4/wrapper/trace/opentelemetry"
|
||||
"github.com/rs/zerolog"
|
||||
"go-micro.dev/v4"
|
||||
"go-micro.dev/v4/client"
|
||||
"go-micro.dev/v4/server"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
|
||||
occrypto "github.com/qsfera/server/pkg/crypto"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/pkg/registry"
|
||||
)
|
||||
|
||||
// Service simply wraps the go-micro grpc service.
|
||||
type Service struct {
|
||||
micro.Service
|
||||
}
|
||||
|
||||
// NewServiceWithClient initializes a new grpc service with explicit client.
|
||||
func NewServiceWithClient(client client.Client, opts ...Option) (Service, error) {
|
||||
var mServer server.Server
|
||||
sopts := newOptions(opts...)
|
||||
keepaliveParams := grpc.KeepaliveParams(keepalive.ServerParameters{
|
||||
MaxConnectionAge: GetMaxConnectionAge(), // this forces clients to reconnect after 30 seconds, triggering a new DNS lookup to pick up new IPs
|
||||
})
|
||||
tlsConfig := &tls.Config{}
|
||||
|
||||
if sopts.TLSEnabled {
|
||||
var cert tls.Certificate
|
||||
var err error
|
||||
if sopts.TLSCert != "" {
|
||||
cert, err = tls.LoadX509KeyPair(sopts.TLSCert, sopts.TLSKey)
|
||||
if err != nil {
|
||||
sopts.Logger.Error().Err(err).Str("cert", sopts.TLSCert).Str("key", sopts.TLSKey).Msg("error loading server certifcate and key")
|
||||
return Service{}, fmt.Errorf("grpc service error loading server certificate and key: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Generate a self-signed server certificate on the fly. This requires the clients
|
||||
// to connect with InsecureSkipVerify.
|
||||
cert, err = occrypto.GenTempCertForAddr(sopts.Address)
|
||||
if err != nil {
|
||||
return Service{}, fmt.Errorf("grpc service error creating temporary self-signed certificate: %w", err)
|
||||
}
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
mServer = mgrpcs.NewServer(mgrpcs.Options(keepaliveParams), mgrpcs.AuthTLS(tlsConfig))
|
||||
} else {
|
||||
mServer = mgrpcs.NewServer(mgrpcs.Options(keepaliveParams))
|
||||
}
|
||||
|
||||
handlerWrappers := []server.HandlerWrapper{
|
||||
mtracer.NewHandlerWrapper(
|
||||
mtracer.WithTraceProvider(sopts.TraceProvider),
|
||||
),
|
||||
}
|
||||
if sopts.Logger.GetLevel() == zerolog.DebugLevel {
|
||||
handlerWrappers = append(handlerWrappers, LogHandler(&sopts.Logger))
|
||||
}
|
||||
handlerWrappers = append(handlerWrappers, sopts.HandlerWrappers...)
|
||||
|
||||
mopts := []micro.Option{
|
||||
// first add a server because it will reset any options
|
||||
micro.Server(mServer),
|
||||
// also add a client that can be used after initializing the service
|
||||
micro.Client(client),
|
||||
micro.Address(sopts.Address),
|
||||
micro.Name(strings.Join([]string{sopts.Namespace, sopts.Name}, ".")),
|
||||
micro.Version(sopts.Version),
|
||||
micro.Context(sopts.Context),
|
||||
micro.Registry(registry.GetRegistry()),
|
||||
micro.RegisterTTL(registry.GetRegisterTTL()),
|
||||
micro.RegisterInterval(registry.GetRegisterInterval()),
|
||||
micro.WrapHandler(prometheus.NewHandlerWrapper()),
|
||||
micro.WrapClient(mtracer.NewClientWrapper(
|
||||
mtracer.WithTraceProvider(sopts.TraceProvider),
|
||||
)),
|
||||
micro.WrapHandler(handlerWrappers...),
|
||||
micro.WrapSubscriber(mtracer.NewSubscriberWrapper(
|
||||
mtracer.WithTraceProvider(sopts.TraceProvider),
|
||||
)),
|
||||
}
|
||||
|
||||
return Service{micro.NewService(mopts...)}, nil
|
||||
}
|
||||
|
||||
// If used with tracing, please ensure this is registered (by micro.WrapHandler()) after
|
||||
// micro-plugin's opentracing wrapper: `opentracing.NewHandlerWrapper()`
|
||||
func LogHandler(l *log.Logger) func(fn server.HandlerFunc) server.HandlerFunc {
|
||||
return func(fn server.HandlerFunc) server.HandlerFunc {
|
||||
return func(ctx context.Context, req server.Request, rsp any) error {
|
||||
now := time.Now()
|
||||
spanContext := trace.SpanContextFromContext(ctx)
|
||||
defer func() {
|
||||
l.Debug().
|
||||
Str("traceid", spanContext.TraceID().String()).
|
||||
Str("method", req.Method()).
|
||||
Str("endpoint", req.Endpoint()).
|
||||
Str("content-type", req.ContentType()).
|
||||
Str("service", req.Service()).
|
||||
Interface("headers", req.Header()).
|
||||
Dur("duration", time.Since(now)).
|
||||
Msg("grpc call")
|
||||
}()
|
||||
return fn(ctx, req, rsp)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user