Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+103
View File
@@ -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)
}
}
}
}
+24
View File
@@ -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
}
+110
View File
@@ -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
}
}
+119
View File
@@ -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)
}
}
}