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
+146
View File
@@ -0,0 +1,146 @@
package debug
import (
"context"
"net/http"
"github.com/qsfera/server/pkg/log"
)
// 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
Context context.Context
Name string
Version string
Address string
Token string
Pprof bool
Zpages bool
Health http.Handler
Ready http.Handler
ConfigDump http.Handler
CorsAllowedOrigins []string
CorsAllowedMethods []string
CorsAllowedHeaders []string
CorsAllowCredentials bool
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
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
}
}
// Context provides a function to set the context option.
func Context(ctx context.Context) Option {
return func(o *Options) {
o.Context = ctx
}
}
// 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
}
}
// Token provides a function to set the token option.
func Token(t string) Option {
return func(o *Options) {
o.Token = t
}
}
// Pprof provides a function to set the pprof option.
func Pprof(p bool) Option {
return func(o *Options) {
o.Pprof = p
}
}
// Zpages provides a function to set the zpages option.
func Zpages(z bool) Option {
return func(o *Options) {
o.Zpages = z
}
}
// Health provides a function to set the health option.
func Health(h http.Handler) Option {
return func(o *Options) {
o.Health = h
}
}
// Ready provides a function to set the ready option.
func Ready(r http.Handler) Option {
return func(o *Options) {
o.Ready = r
}
}
// ConfigDump to be documented.
func ConfigDump(r http.Handler) Option {
return func(o *Options) {
o.ConfigDump = r
}
}
// CorsAllowedOrigins provides a function to set the CorsAllowedOrigin option.
func CorsAllowedOrigins(origins []string) Option {
return func(o *Options) {
o.CorsAllowedOrigins = origins
}
}
// CorsAllowedMethods provides a function to set the CorsAllowedMethods option.
func CorsAllowedMethods(methods []string) Option {
return func(o *Options) {
o.CorsAllowedMethods = methods
}
}
// CorsAllowedHeaders provides a function to set the CorsAllowedHeaders option.
func CorsAllowedHeaders(headers []string) Option {
return func(o *Options) {
o.CorsAllowedHeaders = headers
}
}
// CorsAllowCredentials provides a function to set the CorsAllowAllowCredential option.
func CorsAllowCredentials(allow bool) Option {
return func(o *Options) {
o.CorsAllowCredentials = allow
}
}
+93
View File
@@ -0,0 +1,93 @@
package debug
import (
"context"
"net"
"net/http"
"net/http/pprof"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/justinas/alice"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/contrib/zpages"
"github.com/qsfera/server/pkg/cors"
"github.com/qsfera/server/pkg/handlers"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/middleware"
graphMiddleware "github.com/qsfera/server/services/graph/pkg/middleware"
)
var handleProbe = func(mux *http.ServeMux, pattern string, h http.Handler, logger log.Logger) {
if h == nil {
h = handlers.NewCheckHandler(handlers.NewCheckHandlerConfiguration())
logger.Info().
Str("endpoint", pattern).
Msg("no probe provided, reverting to default (OK)")
}
mux.Handle(pattern, h)
}
// NewService initializes a new debug service.
func NewService(opts ...Option) *http.Server {
dopts := newOptions(opts...)
mux := http.NewServeMux()
mux.Handle("/metrics", alice.New(
graphMiddleware.Token(
dopts.Token,
),
).Then(
promhttp.Handler(),
))
handleProbe(mux, "/healthz", dopts.Health, dopts.Logger) // healthiness check
handleProbe(mux, "/readyz", dopts.Ready, dopts.Logger) // readiness check
if dopts.ConfigDump != nil {
mux.Handle("/config", dopts.ConfigDump)
}
if dopts.Pprof {
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
}
if dopts.Zpages {
h := zpages.NewTracezHandler(zpages.NewSpanProcessor())
mux.Handle("/debug", h)
}
baseCtx := dopts.Context
if baseCtx == nil {
baseCtx = context.Background()
}
return &http.Server{
Addr: dopts.Address,
BaseContext: func(_ net.Listener) context.Context {
return baseCtx
},
Handler: alice.New(
chimiddleware.RealIP,
chimiddleware.RequestID,
middleware.NoCache,
middleware.Cors(
cors.AllowedOrigins(dopts.CorsAllowedOrigins),
cors.AllowedMethods(dopts.CorsAllowedMethods),
cors.AllowedHeaders(dopts.CorsAllowedHeaders),
cors.AllowCredentials(dopts.CorsAllowCredentials),
),
middleware.Version(
dopts.Name,
dopts.Version,
),
).Then(
mux,
),
}
}
+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)
}
}
}
+105
View File
@@ -0,0 +1,105 @@
package http
import (
"context"
"net/http"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/shared"
"github.com/spf13/pflag"
"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
TLSConfig shared.HTTPServiceTLS
Namespace string
Name string
Version string
Address string
Handler http.Handler
Context context.Context
Flags []pflag.Flag
TraceProvider trace.TracerProvider
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{
Namespace: "go.micro.web",
}
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
}
}
// Context provides a function to set the context option.
func Context(ctx context.Context) Option {
return func(o *Options) {
o.Context = ctx
}
}
// Flags provides a function to set the flags option.
func Flags(flags ...pflag.Flag) Option {
return func(o *Options) {
o.Flags = append(o.Flags, flags...)
}
}
// TLSConfig provides a function to set the TLSConfig option.
func TLSConfig(config shared.HTTPServiceTLS) Option {
return func(o *Options) {
o.TLSConfig = config
}
}
// TraceProvider provides a function to set the TraceProvider option.
func TraceProvider(tp trace.TracerProvider) Option {
return func(o *Options) {
o.TraceProvider = tp
}
}
+87
View File
@@ -0,0 +1,87 @@
package http
import (
"crypto/tls"
"fmt"
"strings"
"github.com/qsfera/server/pkg/broker"
"github.com/qsfera/server/pkg/registry"
mhttps "github.com/go-micro/plugins/v4/server/http"
mtracer "github.com/go-micro/plugins/v4/wrapper/trace/opentelemetry"
occrypto "github.com/qsfera/server/pkg/crypto"
"go-micro.dev/v4"
"go-micro.dev/v4/server"
)
// Service simply wraps the go-micro web service.
type Service struct {
micro.Service
}
// NewService initializes a new http service.
func NewService(opts ...Option) (Service, error) {
noopBroker := broker.NoOp{}
sopts := newOptions(opts...)
var mServer server.Server
if sopts.TLSConfig.Enabled {
var cert tls.Certificate
var err error
if sopts.TLSConfig.Cert != "" {
cert, err = tls.LoadX509KeyPair(sopts.TLSConfig.Cert, sopts.TLSConfig.Key)
if err != nil {
sopts.Logger.Error().Err(err).
Str("cert", sopts.TLSConfig.Cert).
Str("key", sopts.TLSConfig.Key).
Msg("error loading server certifcate and key")
return Service{}, fmt.Errorf("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.
sopts.Logger.Warn().Str("address", sopts.Address).
Msg("No server certificate configured. Generating a temporary self-signed certificate")
cert, err = occrypto.GenTempCertForAddr(sopts.Address)
if err != nil {
return Service{}, fmt.Errorf("error creating temporary self-signed certificate: %w", err)
}
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
NextProtos: []string{"h2", "http/1.1"},
}
mServer = mhttps.NewServer(server.TLSConfig(tlsConfig))
} else {
mServer = mhttps.NewServer()
}
wopts := []micro.Option{
micro.Server(mServer),
micro.Broker(noopBroker),
micro.Address(sopts.Address),
micro.Name(strings.Join([]string{sopts.Namespace, sopts.Name}, ".")),
micro.Version(sopts.Version),
micro.Context(sopts.Context),
// TODO: clarify if this is actually used on the go-micro side
//micro.Flags(sopts.Flags...),
micro.Registry(registry.GetRegistry()),
micro.RegisterTTL(registry.GetRegisterTTL()),
micro.RegisterInterval(registry.GetRegisterInterval()),
micro.WrapClient(mtracer.NewClientWrapper(
mtracer.WithTraceProvider(sopts.TraceProvider),
)),
micro.WrapHandler(mtracer.NewHandlerWrapper(
mtracer.WithTraceProvider(sopts.TraceProvider),
)),
micro.WrapSubscriber(mtracer.NewSubscriberWrapper(
mtracer.WithTraceProvider(sopts.TraceProvider),
)),
}
if sopts.TLSConfig.Enabled {
wopts = append(wopts, micro.Metadata(map[string]string{"use_tls": "true"}))
}
return Service{micro.NewService(wopts...)}, nil
}