talos/pkg/grpc/factory/factory.go
Alexey Palazhchenko e60469a38c
feat: initial support for JSON logging
Hook into logging machinery.

Signed-off-by: Alexey Palazhchenko <alexey.palazhchenko@talos-systems.com>
2021-10-16 16:46:59 +00:00

237 lines
6.9 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package factory
import (
"crypto/tls"
"errors"
"fmt"
"io"
"log"
"net"
"os"
"path/filepath"
"runtime/debug"
"strconv"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
grpclog "github.com/talos-systems/talos/pkg/grpc/middleware/log"
_ "github.com/talos-systems/talos/pkg/machinery/proto" //nolint:gci // register codec
)
// Registrator describes the set of methods required in order for a concrete
// type to be used with the Listen function.
type Registrator interface {
Register(*grpc.Server)
}
// Options is the functional options struct.
type Options struct {
Port int
SocketPath string
Network string
Config *tls.Config
ServerOptions []grpc.ServerOption
UnaryInterceptors []grpc.UnaryServerInterceptor
StreamInterceptors []grpc.StreamServerInterceptor
Reflection bool
logPrefix string
logDestination io.Writer
}
// Option is the functional option func.
type Option func(*Options)
// Port sets the listen port of the server.
func Port(o int) Option {
return func(args *Options) {
args.Port = o
}
}
// SocketPath sets the listen unix file socket path of the server.
func SocketPath(o string) Option {
return func(args *Options) {
args.SocketPath = o
}
}
// Network sets the network type of the listener.
func Network(o string) Option {
return func(args *Options) {
args.Network = o
}
}
// Config sets the listen port of the server.
func Config(o *tls.Config) Option {
return func(args *Options) {
args.Config = o
}
}
// ServerOptions appends to the gRPC server options of the server.
func ServerOptions(o ...grpc.ServerOption) Option {
return func(args *Options) {
args.ServerOptions = append(args.ServerOptions, o...)
}
}
// WithUnaryInterceptor appends to the list of gRPC server unary interceptors.
func WithUnaryInterceptor(i grpc.UnaryServerInterceptor) Option {
return func(args *Options) {
args.UnaryInterceptors = append(args.UnaryInterceptors, i)
}
}
// WithStreamInterceptor appends to the list of gRPC server stream interceptors.
func WithStreamInterceptor(i grpc.StreamServerInterceptor) Option {
return func(args *Options) {
args.StreamInterceptors = append(args.StreamInterceptors, i)
}
}
// WithLog sets up request logging to specified destination.
func WithLog(prefix string, w io.Writer) Option {
return func(args *Options) {
args.logPrefix = prefix
args.logDestination = w
}
}
// WithDefaultLog sets up request logging to default destination.
func WithDefaultLog() Option {
return func(args *Options) {
args.logDestination = log.Writer()
}
}
// WithReflection enables gRPC reflection APIs: https://github.com/grpc/grpc/blob/master/doc/server-reflection.md
func WithReflection() Option {
return func(args *Options) {
args.Reflection = true
}
}
func recoveryHandler(logger *log.Logger) grpc_recovery.RecoveryHandlerFunc {
return func(p interface{}) error {
if logger != nil {
logger.Printf("panic: %v\n%s", p, string(debug.Stack()))
}
return status.Errorf(codes.Internal, "%v", p)
}
}
// NewDefaultOptions initializes the Options struct with default values.
func NewDefaultOptions(setters ...Option) *Options {
opts := &Options{
Network: "tcp",
SocketPath: "/run/factory/factory.sock",
}
for _, setter := range setters {
setter(opts)
}
var logger *log.Logger
if opts.logDestination != nil {
logger = log.New(opts.logDestination, opts.logPrefix, log.Flags())
}
// Recovery is installed as the the first middleware in the chain to handle panics (via defer and recover()) in all subsequent middlewares.
recoveryOpt := grpc_recovery.WithRecoveryHandler(recoveryHandler(logger))
opts.UnaryInterceptors = append([]grpc.UnaryServerInterceptor{grpc_recovery.UnaryServerInterceptor(recoveryOpt)}, opts.UnaryInterceptors...)
opts.StreamInterceptors = append([]grpc.StreamServerInterceptor{grpc_recovery.StreamServerInterceptor(recoveryOpt)}, opts.StreamInterceptors...)
if logger != nil {
// Logging is installed as the first middleware (even before recovery middleware) in the chain
// so that request in the form it was received and status sent on the wire is logged (error/success).
// It also tracks the whole duration of the request, including other middleware overhead.
logMiddleware := grpclog.NewMiddleware(logger)
opts.UnaryInterceptors = append([]grpc.UnaryServerInterceptor{logMiddleware.UnaryInterceptor()}, opts.UnaryInterceptors...)
opts.StreamInterceptors = append([]grpc.StreamServerInterceptor{logMiddleware.StreamInterceptor()}, opts.StreamInterceptors...)
}
opts.ServerOptions = append(opts.ServerOptions,
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(opts.UnaryInterceptors...)),
grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(opts.StreamInterceptors...)),
)
return opts
}
// NewServer builds grpc server and binds it to the Registrator.
func NewServer(r Registrator, setters ...Option) *grpc.Server {
opts := NewDefaultOptions(setters...)
server := grpc.NewServer(opts.ServerOptions...)
r.Register(server)
if opts.Reflection {
reflection.Register(server)
}
return server
}
// NewListener builds listener for grpc server.
func NewListener(setters ...Option) (net.Listener, error) {
opts := NewDefaultOptions(setters...)
if opts.Network == "tcp" && opts.Port == 0 {
return nil, errors.New("a port is required for TCP listener")
}
var address string
switch opts.Network {
case "unix":
address = opts.SocketPath
// Unlink the address or we will get the error:
// bind: address already in use.
if _, err := os.Stat(address); err == nil {
if err := os.Remove(address); err != nil {
return nil, err
}
}
// Make any dirs on the path to the listening socket.
if err := os.MkdirAll(filepath.Dir(address), 0o700); err != nil {
return nil, fmt.Errorf("error creating containing directory for the file socket; %w", err)
}
case "tcp":
address = ":" + strconv.Itoa(opts.Port)
default:
return nil, fmt.Errorf("unknown network: %s", opts.Network)
}
return net.Listen(opts.Network, address)
}
// ListenAndServe configures TLS for mutual authentication by loading the CA into a
// CertPool and configuring the server's policy for TLS Client Authentication.
// Once TLS is configured, the gRPC options are built to make use of the TLS
// configuration and the receiver (Server) is registered to the gRPC server.
// Finally the gRPC server is started.
func ListenAndServe(r Registrator, setters ...Option) (err error) {
server := NewServer(r, setters...)
listener, err := NewListener(setters...)
if err != nil {
return err
}
return server.Serve(listener)
}