chore: upstream more changes from v2 (#20387)
Co-authored-by: Matt Kocubinski <mkocubinski@gmail.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package grpc
|
||||
|
||||
import "math"
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Enable: true,
|
||||
// DefaultGRPCAddress defines the default address to bind the gRPC server to.
|
||||
Address: "localhost:9090",
|
||||
// DefaultGRPCMaxRecvMsgSize defines the default gRPC max message size in
|
||||
// bytes the server can receive.
|
||||
MaxRecvMsgSize: 1024 * 1024 * 10,
|
||||
// DefaultGRPCMaxSendMsgSize defines the default gRPC max message size in
|
||||
// bytes the server can send.
|
||||
MaxSendMsgSize: math.MaxInt32,
|
||||
}
|
||||
}
|
||||
|
||||
// GRPCConfig defines configuration for the gRPC server.
|
||||
type Config struct {
|
||||
// Enable defines if the gRPC server should be enabled.
|
||||
Enable bool `mapstructure:"enable" toml:"enable" comment:"Enable defines if the gRPC server should be enabled."`
|
||||
|
||||
// Address defines the API server to listen on
|
||||
Address string `mapstructure:"address" toml:"address" comment:"Address defines the gRPC server address to bind to."`
|
||||
|
||||
// MaxRecvMsgSize defines the max message size in bytes the server can receive.
|
||||
// The default value is 10MB.
|
||||
MaxRecvMsgSize int `mapstructure:"max-recv-msg-size" toml:"max-recv-msg-size" comment:"MaxRecvMsgSize defines the max message size in bytes the server can receive.\nThe default value is 10MB."`
|
||||
|
||||
// MaxSendMsgSize defines the max message size in bytes the server can send.
|
||||
// The default value is math.MaxInt32.
|
||||
MaxSendMsgSize int `mapstructure:"max-send-msg-size" toml:"max-send-msg-size" comment:"MaxSendMsgSize defines the max message size in bytes the server can send.\nThe default value is math.MaxInt32."`
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Package gogoreflection implements gRPC reflection for gogoproto consumers
|
||||
// the normal reflection library does not work as it points to a different
|
||||
// singleton registry. The API and codebase is taken from the official gRPC
|
||||
// reflection repository.
|
||||
package gogoreflection
|
||||
@@ -0,0 +1,76 @@
|
||||
package gogoreflection
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
_ "github.com/cosmos/gogoproto/gogoproto" // required so it does register the gogoproto file descriptor
|
||||
gogoproto "github.com/cosmos/gogoproto/proto"
|
||||
|
||||
_ "github.com/cosmos/cosmos-proto" // look above
|
||||
"github.com/golang/protobuf/proto" //nolint:staticcheck // migrate in a future pr
|
||||
)
|
||||
|
||||
func getFileDescriptor(filePath string) []byte {
|
||||
// Since we got well known descriptors which are not registered into gogoproto
|
||||
// registry but are instead registered into the proto one, we need to check both.
|
||||
fd := gogoproto.FileDescriptor(filePath)
|
||||
if len(fd) != 0 {
|
||||
return fd
|
||||
}
|
||||
|
||||
return proto.FileDescriptor(filePath) //nolint:staticcheck // keep for backward compatibility
|
||||
}
|
||||
|
||||
func getMessageType(name string) reflect.Type {
|
||||
typ := gogoproto.MessageType(name)
|
||||
if typ != nil {
|
||||
return typ
|
||||
}
|
||||
|
||||
return proto.MessageType(name) //nolint:staticcheck // keep for backward compatibility
|
||||
}
|
||||
|
||||
func getExtension(extID int32, m proto.Message) *gogoproto.ExtensionDesc {
|
||||
// check first in gogoproto registry
|
||||
for id, desc := range gogoproto.RegisteredExtensions(m) {
|
||||
if id == extID {
|
||||
return desc
|
||||
}
|
||||
}
|
||||
|
||||
// check into proto registry
|
||||
for id, desc := range proto.RegisteredExtensions(m) { //nolint:staticcheck // keep for backward compatibility
|
||||
if id == extID {
|
||||
return &gogoproto.ExtensionDesc{
|
||||
ExtendedType: desc.ExtendedType, //nolint:staticcheck // keep for backward compatibility
|
||||
ExtensionType: desc.ExtensionType, //nolint:staticcheck // keep for backward compatibility
|
||||
Field: desc.Field, //nolint:staticcheck // keep for backward compatibility
|
||||
Name: desc.Name, //nolint:staticcheck // keep for backward compatibility
|
||||
Tag: desc.Tag, //nolint:staticcheck // keep for backward compatibility
|
||||
Filename: desc.Filename, //nolint:staticcheck // keep for backward compatibility
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getExtensionsNumbers(m proto.Message) []int32 {
|
||||
gogoProtoExts := gogoproto.RegisteredExtensions(m)
|
||||
|
||||
out := make([]int32, 0, len(gogoProtoExts))
|
||||
for id := range gogoProtoExts {
|
||||
out = append(out, id)
|
||||
}
|
||||
if len(out) != 0 {
|
||||
return out
|
||||
}
|
||||
|
||||
protoExts := proto.RegisteredExtensions(m) //nolint:staticcheck // kept for backwards compatibility
|
||||
out = make([]int32, 0, len(protoExts))
|
||||
for id := range protoExts {
|
||||
out = append(out, id)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package gogoreflection
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"google.golang.org/protobuf/runtime/protoimpl"
|
||||
)
|
||||
|
||||
func TestRegistrationFix(t *testing.T) {
|
||||
res := getFileDescriptor("gogoproto/gogo.proto")
|
||||
rawDesc, err := decompress(res)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fd := protoimpl.DescBuilder{
|
||||
RawDescriptor: rawDesc,
|
||||
}.Build()
|
||||
|
||||
if fd.File.Extensions().Len() == 0 {
|
||||
t.Fatal("unexpected parsing")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2016 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
Package reflection implements server reflection service.
|
||||
|
||||
The service implemented is defined in:
|
||||
https://github.com/grpc/grpc/blob/master/src/proto/grpc/reflection/v1alpha/reflection.proto.
|
||||
|
||||
To register server reflection on a gRPC server:
|
||||
|
||||
import "google.golang.org/grpc/reflection"
|
||||
|
||||
s := grpc.NewServer()
|
||||
pb.RegisterYourOwnServer(s, &server{})
|
||||
|
||||
// Register reflection service on gRPC server.
|
||||
reflection.Register(s)
|
||||
|
||||
s.Serve(lis)
|
||||
*/
|
||||
package gogoreflection // import "google.golang.org/grpc/reflection"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
//nolint: staticcheck // keep this import for backward compatibility
|
||||
"github.com/golang/protobuf/proto"
|
||||
dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
rpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type serverReflectionServer struct {
|
||||
rpb.UnimplementedServerReflectionServer
|
||||
s *grpc.Server
|
||||
|
||||
initSymbols sync.Once
|
||||
serviceNames []string
|
||||
symbols map[string]*dpb.FileDescriptorProto // map of fully-qualified names to files
|
||||
}
|
||||
|
||||
// Register registers the server reflection service on the given gRPC server.
|
||||
func Register(s *grpc.Server) {
|
||||
rpb.RegisterServerReflectionServer(s, &serverReflectionServer{
|
||||
s: s,
|
||||
})
|
||||
}
|
||||
|
||||
// protoMessage is used for type assertion on proto messages.
|
||||
// Generated proto message implements function Descriptor(), but Descriptor()
|
||||
// is not part of interface proto.Message. This interface is needed to
|
||||
// call Descriptor().
|
||||
type protoMessage interface {
|
||||
Descriptor() ([]byte, []int)
|
||||
}
|
||||
|
||||
func (s *serverReflectionServer) getSymbols() (svcNames []string, symbolIndex map[string]*dpb.FileDescriptorProto) {
|
||||
s.initSymbols.Do(func() {
|
||||
serviceInfo := s.s.GetServiceInfo()
|
||||
|
||||
s.symbols = map[string]*dpb.FileDescriptorProto{}
|
||||
s.serviceNames = make([]string, 0, len(serviceInfo))
|
||||
processed := map[string]struct{}{}
|
||||
for svc, info := range serviceInfo {
|
||||
s.serviceNames = append(s.serviceNames, svc)
|
||||
fdenc, ok := parseMetadata(info.Metadata)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fd, err := decodeFileDesc(fdenc)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.processFile(fd, processed)
|
||||
}
|
||||
sort.Strings(s.serviceNames)
|
||||
})
|
||||
|
||||
return s.serviceNames, s.symbols
|
||||
}
|
||||
|
||||
func (s *serverReflectionServer) processFile(fd *dpb.FileDescriptorProto, processed map[string]struct{}) {
|
||||
filename := fd.GetName()
|
||||
if _, ok := processed[filename]; ok {
|
||||
return
|
||||
}
|
||||
processed[filename] = struct{}{}
|
||||
|
||||
prefix := fd.GetPackage()
|
||||
|
||||
for _, msg := range fd.MessageType {
|
||||
s.processMessage(fd, prefix, msg)
|
||||
}
|
||||
for _, en := range fd.EnumType {
|
||||
s.processEnum(fd, prefix, en)
|
||||
}
|
||||
for _, ext := range fd.Extension {
|
||||
s.processField(fd, prefix, ext)
|
||||
}
|
||||
for _, svc := range fd.Service {
|
||||
svcName := fqn(prefix, svc.GetName())
|
||||
s.symbols[svcName] = fd
|
||||
for _, meth := range svc.Method {
|
||||
name := fqn(svcName, meth.GetName())
|
||||
s.symbols[name] = fd
|
||||
}
|
||||
}
|
||||
|
||||
for _, dep := range fd.Dependency {
|
||||
fdenc := getFileDescriptor(dep)
|
||||
fdDep, err := decodeFileDesc(fdenc)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.processFile(fdDep, processed)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serverReflectionServer) processMessage(fd *dpb.FileDescriptorProto, prefix string, msg *dpb.DescriptorProto) {
|
||||
msgName := fqn(prefix, msg.GetName())
|
||||
s.symbols[msgName] = fd
|
||||
|
||||
for _, nested := range msg.NestedType {
|
||||
s.processMessage(fd, msgName, nested)
|
||||
}
|
||||
for _, en := range msg.EnumType {
|
||||
s.processEnum(fd, msgName, en)
|
||||
}
|
||||
for _, ext := range msg.Extension {
|
||||
s.processField(fd, msgName, ext)
|
||||
}
|
||||
for _, fld := range msg.Field {
|
||||
s.processField(fd, msgName, fld)
|
||||
}
|
||||
for _, oneof := range msg.OneofDecl {
|
||||
oneofName := fqn(msgName, oneof.GetName())
|
||||
s.symbols[oneofName] = fd
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serverReflectionServer) processEnum(fd *dpb.FileDescriptorProto, prefix string, en *dpb.EnumDescriptorProto) {
|
||||
enName := fqn(prefix, en.GetName())
|
||||
s.symbols[enName] = fd
|
||||
|
||||
for _, val := range en.Value {
|
||||
valName := fqn(enName, val.GetName())
|
||||
s.symbols[valName] = fd
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serverReflectionServer) processField(fd *dpb.FileDescriptorProto, prefix string, fld *dpb.FieldDescriptorProto) {
|
||||
fldName := fqn(prefix, fld.GetName())
|
||||
s.symbols[fldName] = fd
|
||||
}
|
||||
|
||||
func fqn(prefix, name string) string {
|
||||
if prefix == "" {
|
||||
return name
|
||||
}
|
||||
return prefix + "." + name
|
||||
}
|
||||
|
||||
// fileDescForType gets the file descriptor for the given type.
|
||||
// The given type should be a proto message.
|
||||
func (s *serverReflectionServer) fileDescForType(st reflect.Type) (*dpb.FileDescriptorProto, error) {
|
||||
m, ok := reflect.Zero(reflect.PtrTo(st)).Interface().(protoMessage)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to create message from type: %v", st)
|
||||
}
|
||||
enc, _ := m.Descriptor()
|
||||
|
||||
return decodeFileDesc(enc)
|
||||
}
|
||||
|
||||
// decodeFileDesc does decompression and unmarshalling on the given
|
||||
// file descriptor byte slice.
|
||||
func decodeFileDesc(enc []byte) (*dpb.FileDescriptorProto, error) {
|
||||
raw, err := decompress(enc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decompress enc: %w", err)
|
||||
}
|
||||
|
||||
fd := new(dpb.FileDescriptorProto)
|
||||
if err := proto.Unmarshal(raw, fd); err != nil {
|
||||
return nil, fmt.Errorf("bad descriptor: %w", err)
|
||||
}
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
// decompress does gzip decompression.
|
||||
func decompress(b []byte) ([]byte, error) {
|
||||
r, err := gzip.NewReader(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bad gzipped descriptor: %w", err)
|
||||
}
|
||||
out, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bad gzipped descriptor: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func typeForName(name string) (reflect.Type, error) {
|
||||
pt := getMessageType(name)
|
||||
if pt == nil {
|
||||
return nil, fmt.Errorf("unknown type: %q", name)
|
||||
}
|
||||
st := pt.Elem()
|
||||
|
||||
return st, nil
|
||||
}
|
||||
|
||||
func fileDescContainingExtension(st reflect.Type, ext int32) (*dpb.FileDescriptorProto, error) {
|
||||
m, ok := reflect.Zero(reflect.PtrTo(st)).Interface().(proto.Message)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to create message from type: %v", st)
|
||||
}
|
||||
|
||||
extDesc := getExtension(ext, m)
|
||||
|
||||
if extDesc == nil {
|
||||
return nil, fmt.Errorf("failed to find registered extension for extension number %v", ext)
|
||||
}
|
||||
|
||||
return decodeFileDesc(getFileDescriptor(extDesc.Filename))
|
||||
}
|
||||
|
||||
func (s *serverReflectionServer) allExtensionNumbersForType(st reflect.Type) ([]int32, error) {
|
||||
m, ok := reflect.Zero(reflect.PtrTo(st)).Interface().(proto.Message)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to create message from type: %v", st)
|
||||
}
|
||||
|
||||
out := getExtensionsNumbers(m)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// fileDescWithDependencies returns a slice of serialized fileDescriptors in
|
||||
// wire format ([]byte). The fileDescriptors will include fd and all the
|
||||
// transitive dependencies of fd with names not in sentFileDescriptors.
|
||||
func fileDescWithDependencies(fd *dpb.FileDescriptorProto, sentFileDescriptors map[string]bool) ([][]byte, error) {
|
||||
r := [][]byte{}
|
||||
queue := []*dpb.FileDescriptorProto{fd}
|
||||
for len(queue) > 0 {
|
||||
currentfd := queue[0]
|
||||
queue = queue[1:]
|
||||
if sent := sentFileDescriptors[currentfd.GetName()]; len(r) == 0 || !sent {
|
||||
sentFileDescriptors[currentfd.GetName()] = true
|
||||
currentfdEncoded, err := proto.Marshal(currentfd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r = append(r, currentfdEncoded)
|
||||
}
|
||||
for _, dep := range currentfd.Dependency {
|
||||
fdenc := getFileDescriptor(dep)
|
||||
fdDep, err := decodeFileDesc(fdenc)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
queue = append(queue, fdDep)
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// fileDescEncodingByFilename finds the file descriptor for given filename,
|
||||
// finds all of its previously unsent transitive dependencies, does marshaling
|
||||
// on them, and returns the marshaled result.
|
||||
func (s *serverReflectionServer) fileDescEncodingByFilename(name string, sentFileDescriptors map[string]bool) ([][]byte, error) {
|
||||
enc := getFileDescriptor(name)
|
||||
if enc == nil {
|
||||
return nil, fmt.Errorf("unknown file: %v", name)
|
||||
}
|
||||
fd, err := decodeFileDesc(enc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileDescWithDependencies(fd, sentFileDescriptors)
|
||||
}
|
||||
|
||||
// parseMetadata finds the file descriptor bytes specified meta.
|
||||
// For SupportPackageIsVersion4, m is the name of the proto file, we
|
||||
// call proto.FileDescriptor to get the byte slice.
|
||||
// For SupportPackageIsVersion3, m is a byte slice itself.
|
||||
func parseMetadata(meta interface{}) ([]byte, bool) {
|
||||
// Check if meta is the file name.
|
||||
if fileNameForMeta, ok := meta.(string); ok {
|
||||
return getFileDescriptor(fileNameForMeta), true
|
||||
}
|
||||
|
||||
// Check if meta is the byte slice.
|
||||
if enc, ok := meta.([]byte); ok {
|
||||
return enc, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// fileDescEncodingContainingSymbol finds the file descriptor containing the
|
||||
// given symbol, finds all of its previously unsent transitive dependencies,
|
||||
// does marshaling on them, and returns the marshaled result. The given symbol
|
||||
// can be a type, a service or a method.
|
||||
func (s *serverReflectionServer) fileDescEncodingContainingSymbol(name string, sentFileDescriptors map[string]bool) ([][]byte, error) {
|
||||
_, symbols := s.getSymbols()
|
||||
fd := symbols[name]
|
||||
if fd == nil {
|
||||
// Check if it's a type name that was not present in the
|
||||
// transitive dependencies of the registered services.
|
||||
if st, err := typeForName(name); err == nil {
|
||||
fd, err = s.fileDescForType(st)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fd == nil {
|
||||
return nil, fmt.Errorf("unknown symbol: %v", name)
|
||||
}
|
||||
|
||||
return fileDescWithDependencies(fd, sentFileDescriptors)
|
||||
}
|
||||
|
||||
// fileDescEncodingContainingExtension finds the file descriptor containing
|
||||
// given extension, finds all of its previously unsent transitive dependencies,
|
||||
// does marshaling on them, and returns the marshaled result.
|
||||
func (s *serverReflectionServer) fileDescEncodingContainingExtension(typeName string, extNum int32, sentFileDescriptors map[string]bool) ([][]byte, error) {
|
||||
st, err := typeForName(typeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, err := fileDescContainingExtension(st, extNum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileDescWithDependencies(fd, sentFileDescriptors)
|
||||
}
|
||||
|
||||
// allExtensionNumbersForTypeName returns all extension numbers for the given type.
|
||||
func (s *serverReflectionServer) allExtensionNumbersForTypeName(name string) ([]int32, error) {
|
||||
st, err := typeForName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extNums, err := s.allExtensionNumbersForType(st)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return extNums, nil
|
||||
}
|
||||
|
||||
// ServerReflectionInfo is the reflection service handler.
|
||||
func (s *serverReflectionServer) ServerReflectionInfo(stream rpb.ServerReflection_ServerReflectionInfoServer) error {
|
||||
sentFileDescriptors := make(map[string]bool)
|
||||
for {
|
||||
in, err := stream.Recv()
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out := &rpb.ServerReflectionResponse{ //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
ValidHost: in.Host, //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
OriginalRequest: in,
|
||||
}
|
||||
switch req := in.MessageRequest.(type) {
|
||||
case *rpb.ServerReflectionRequest_FileByFilename:
|
||||
b, err := s.fileDescEncodingByFilename(req.FileByFilename, sentFileDescriptors) //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
if err != nil {
|
||||
out.MessageResponse = &rpb.ServerReflectionResponse_ErrorResponse{
|
||||
ErrorResponse: &rpb.ErrorResponse{ //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
ErrorCode: int32(codes.NotFound),
|
||||
ErrorMessage: err.Error(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
out.MessageResponse = &rpb.ServerReflectionResponse_FileDescriptorResponse{
|
||||
FileDescriptorResponse: &rpb.FileDescriptorResponse{FileDescriptorProto: b}, //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
}
|
||||
}
|
||||
case *rpb.ServerReflectionRequest_FileContainingSymbol:
|
||||
b, err := s.fileDescEncodingContainingSymbol(req.FileContainingSymbol, sentFileDescriptors) //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
if err != nil {
|
||||
out.MessageResponse = &rpb.ServerReflectionResponse_ErrorResponse{
|
||||
ErrorResponse: &rpb.ErrorResponse{ //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
ErrorCode: int32(codes.NotFound),
|
||||
ErrorMessage: err.Error(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
out.MessageResponse = &rpb.ServerReflectionResponse_FileDescriptorResponse{
|
||||
FileDescriptorResponse: &rpb.FileDescriptorResponse{FileDescriptorProto: b}, //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
}
|
||||
}
|
||||
case *rpb.ServerReflectionRequest_FileContainingExtension:
|
||||
typeName := req.FileContainingExtension.ContainingType //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
extNum := req.FileContainingExtension.ExtensionNumber //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
b, err := s.fileDescEncodingContainingExtension(typeName, extNum, sentFileDescriptors)
|
||||
if err != nil {
|
||||
out.MessageResponse = &rpb.ServerReflectionResponse_ErrorResponse{
|
||||
ErrorResponse: &rpb.ErrorResponse{ //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
ErrorCode: int32(codes.NotFound),
|
||||
ErrorMessage: err.Error(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
out.MessageResponse = &rpb.ServerReflectionResponse_FileDescriptorResponse{
|
||||
FileDescriptorResponse: &rpb.FileDescriptorResponse{FileDescriptorProto: b}, //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
}
|
||||
}
|
||||
case *rpb.ServerReflectionRequest_AllExtensionNumbersOfType:
|
||||
extNums, err := s.allExtensionNumbersForTypeName(req.AllExtensionNumbersOfType) //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
if err != nil {
|
||||
out.MessageResponse = &rpb.ServerReflectionResponse_ErrorResponse{
|
||||
ErrorResponse: &rpb.ErrorResponse{ //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
ErrorCode: int32(codes.NotFound),
|
||||
ErrorMessage: err.Error(),
|
||||
},
|
||||
}
|
||||
log.Printf("OH NO: %s", err)
|
||||
} else {
|
||||
out.MessageResponse = &rpb.ServerReflectionResponse_AllExtensionNumbersResponse{
|
||||
AllExtensionNumbersResponse: &rpb.ExtensionNumberResponse{ //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
BaseTypeName: req.AllExtensionNumbersOfType, //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
ExtensionNumber: extNums,
|
||||
},
|
||||
}
|
||||
}
|
||||
case *rpb.ServerReflectionRequest_ListServices:
|
||||
svcNames, _ := s.getSymbols()
|
||||
serviceResponses := make([]*rpb.ServiceResponse, len(svcNames)) //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
for i, n := range svcNames {
|
||||
serviceResponses[i] = &rpb.ServiceResponse{ //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
Name: n,
|
||||
}
|
||||
}
|
||||
out.MessageResponse = &rpb.ServerReflectionResponse_ListServicesResponse{
|
||||
ListServicesResponse: &rpb.ListServiceResponse{ //nolint:staticcheck // SA1019: we want to keep using v1alpha
|
||||
Service: serviceResponses,
|
||||
},
|
||||
}
|
||||
default:
|
||||
return status.Errorf(codes.InvalidArgument, "invalid MessageRequest: %v", in.MessageRequest)
|
||||
}
|
||||
if err := stream.Send(out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
gogogrpc "github.com/cosmos/gogoproto/grpc"
|
||||
gogoproto "github.com/cosmos/gogoproto/proto"
|
||||
"github.com/spf13/viper"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/encoding"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
_ "cosmossdk.io/api/amino" // Import amino.proto file for reflection
|
||||
appmanager "cosmossdk.io/core/app"
|
||||
"cosmossdk.io/log"
|
||||
"cosmossdk.io/server/v2/api/grpc/gogoreflection"
|
||||
)
|
||||
|
||||
const serverName = "grpc-server"
|
||||
|
||||
type GRPCServer struct {
|
||||
logger log.Logger
|
||||
|
||||
grpcSrv *grpc.Server
|
||||
config *Config
|
||||
}
|
||||
|
||||
type GRPCService interface {
|
||||
// RegisterGRPCServer registers gRPC services directly with the gRPC server.
|
||||
RegisterGRPCServer(gogogrpc.Server)
|
||||
}
|
||||
|
||||
// New returns a correctly configured and initialized gRPC server.
|
||||
// Note, the caller is responsible for starting the server.
|
||||
func New(logger log.Logger, v *viper.Viper, interfaceRegistry appmanager.InterfaceRegistry, app GRPCService) (GRPCServer, error) {
|
||||
cfg := DefaultConfig()
|
||||
if v != nil {
|
||||
if err := v.Sub(serverName).Unmarshal(&cfg); err != nil {
|
||||
return GRPCServer{}, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
grpcSrv := grpc.NewServer(
|
||||
grpc.ForceServerCodec(newProtoCodec(interfaceRegistry).GRPCCodec()),
|
||||
grpc.MaxSendMsgSize(cfg.MaxSendMsgSize),
|
||||
grpc.MaxRecvMsgSize(cfg.MaxRecvMsgSize),
|
||||
)
|
||||
|
||||
app.RegisterGRPCServer(grpcSrv)
|
||||
|
||||
// Reflection allows external clients to see what services and methods
|
||||
// the gRPC server exposes.
|
||||
gogoreflection.Register(grpcSrv)
|
||||
|
||||
return GRPCServer{
|
||||
grpcSrv: grpcSrv,
|
||||
config: cfg,
|
||||
logger: logger.With(log.ModuleKey, serverName),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g GRPCServer) Name() string {
|
||||
return serverName
|
||||
}
|
||||
|
||||
func (g GRPCServer) Start(ctx context.Context) error {
|
||||
listener, err := net.Listen("tcp", g.config.Address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on address %s: %w", g.config.Address, err)
|
||||
}
|
||||
|
||||
errCh := make(chan error)
|
||||
|
||||
// Start the gRPC in an external goroutine as Serve is blocking and will return
|
||||
// an error upon failure, which we'll send on the error channel that will be
|
||||
// consumed by the for block below.
|
||||
go func() {
|
||||
g.logger.Info("starting gRPC server...", "address", g.config.Address)
|
||||
errCh <- g.grpcSrv.Serve(listener)
|
||||
}()
|
||||
|
||||
// Start a blocking select to wait for an indication to stop the server or that
|
||||
// the server failed to start properly.
|
||||
err = <-errCh
|
||||
g.logger.Error("failed to start gRPC server", "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (g GRPCServer) Stop(ctx context.Context) error {
|
||||
g.logger.Info("stopping gRPC server...", "address", g.config.Address)
|
||||
g.grpcSrv.GracefulStop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g GRPCServer) Config() any {
|
||||
if g.config == nil || g.config == (&Config{}) {
|
||||
return DefaultConfig()
|
||||
}
|
||||
|
||||
return g.config
|
||||
}
|
||||
|
||||
type protoCodec struct {
|
||||
interfaceRegistry appmanager.InterfaceRegistry
|
||||
}
|
||||
|
||||
// newProtoCodec returns a reference to a new ProtoCodec
|
||||
func newProtoCodec(interfaceRegistry appmanager.InterfaceRegistry) *protoCodec {
|
||||
return &protoCodec{
|
||||
interfaceRegistry: interfaceRegistry,
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal implements BinaryMarshaler.Marshal method.
|
||||
// NOTE: this function must be used with a concrete type which
|
||||
// implements proto.Message. For interface please use the codec.MarshalInterface
|
||||
func (pc *protoCodec) Marshal(o gogoproto.Message) ([]byte, error) {
|
||||
// Size() check can catch the typed nil value.
|
||||
if o == nil || gogoproto.Size(o) == 0 {
|
||||
// return empty bytes instead of nil, because nil has special meaning in places like store.Set
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
return gogoproto.Marshal(o)
|
||||
}
|
||||
|
||||
// Unmarshal implements BinaryMarshaler.Unmarshal method.
|
||||
// NOTE: this function must be used with a concrete type which
|
||||
// implements proto.Message. For interface please use the codec.UnmarshalInterface
|
||||
func (pc *protoCodec) Unmarshal(bz []byte, ptr gogoproto.Message) error {
|
||||
err := gogoproto.Unmarshal(bz, ptr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// err = codectypes.UnpackInterfaces(ptr, pc.interfaceRegistry) // TODO: identify if needed for grpc
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pc *protoCodec) Name() string {
|
||||
return "cosmos-sdk-grpc-codec"
|
||||
}
|
||||
|
||||
// GRPCCodec returns the gRPC Codec for this specific ProtoCodec
|
||||
func (pc *protoCodec) GRPCCodec() encoding.Codec {
|
||||
return &grpcProtoCodec{cdc: pc}
|
||||
}
|
||||
|
||||
// grpcProtoCodec is the implementation of the gRPC proto codec.
|
||||
type grpcProtoCodec struct {
|
||||
cdc appmanager.ProtoCodec
|
||||
}
|
||||
|
||||
var errUnknownProtoType = errors.New("codec: unknown proto type") // sentinel error
|
||||
|
||||
func (g grpcProtoCodec) Marshal(v any) ([]byte, error) {
|
||||
switch m := v.(type) {
|
||||
case proto.Message:
|
||||
protov2MarshalOpts := proto.MarshalOptions{Deterministic: true}
|
||||
return protov2MarshalOpts.Marshal(m)
|
||||
case gogoproto.Message:
|
||||
return g.cdc.Marshal(m)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: cannot marshal type %T", errUnknownProtoType, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (g grpcProtoCodec) Unmarshal(data []byte, v any) error {
|
||||
switch m := v.(type) {
|
||||
case proto.Message:
|
||||
return proto.Unmarshal(data, m)
|
||||
case gogoproto.Message:
|
||||
return g.cdc.Unmarshal(data, m)
|
||||
default:
|
||||
return fmt.Errorf("%w: cannot unmarshal type %T", errUnknownProtoType, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (g grpcProtoCodec) Name() string {
|
||||
return "cosmos-sdk-grpc-codec"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package grpcgateway
|
||||
|
||||
type Config struct {
|
||||
// Enable defines if the gRPC-gateway should be enabled.
|
||||
Enable bool `mapstructure:"enable" toml:"enable" comment:"Enable defines if the gRPC-gateway should be enabled."`
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Enable: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package grpcgateway
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
gateway "github.com/cosmos/gogogateway"
|
||||
"github.com/cosmos/gogoproto/jsonpb"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/runtime"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"cosmossdk.io/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// GRPCBlockHeightHeader is the gRPC header for block height.
|
||||
GRPCBlockHeightHeader = "x-cosmos-block-height"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
logger log.Logger
|
||||
GRPCSrv *grpc.Server
|
||||
GRPCGatewayRouter *runtime.ServeMux
|
||||
config Config
|
||||
}
|
||||
|
||||
// New creates a new gRPC-gateway server.
|
||||
func New(logger log.Logger, grpcSrv *grpc.Server, cfg Config, ir jsonpb.AnyResolver) *Server {
|
||||
// The default JSON marshaller used by the gRPC-Gateway is unable to marshal non-nullable non-scalar fields.
|
||||
// Using the gogo/gateway package with the gRPC-Gateway WithMarshaler option fixes the scalar field marshaling issue.
|
||||
marshalerOption := &gateway.JSONPb{
|
||||
EmitDefaults: true,
|
||||
Indent: "",
|
||||
OrigName: true,
|
||||
AnyResolver: ir,
|
||||
}
|
||||
return &Server{
|
||||
logger: logger,
|
||||
GRPCGatewayRouter: runtime.NewServeMux(
|
||||
// Custom marshaler option is required for gogo proto
|
||||
runtime.WithMarshalerOption(runtime.MIMEWildcard, marshalerOption),
|
||||
|
||||
// This is necessary to get error details properly
|
||||
// marshaled in unary requests.
|
||||
runtime.WithProtoErrorHandler(runtime.DefaultHTTPProtoErrorHandler),
|
||||
|
||||
// Custom header matcher for mapping request headers to
|
||||
// GRPC metadata
|
||||
runtime.WithIncomingHeaderMatcher(CustomGRPCHeaderMatcher),
|
||||
),
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// CustomGRPCHeaderMatcher for mapping request headers to
|
||||
// GRPC metadata.
|
||||
// HTTP headers that start with 'Grpc-Metadata-' are automatically mapped to
|
||||
// gRPC metadata after removing prefix 'Grpc-Metadata-'. We can use this
|
||||
// CustomGRPCHeaderMatcher if headers don't start with `Grpc-Metadata-`
|
||||
func CustomGRPCHeaderMatcher(key string) (string, bool) {
|
||||
switch strings.ToLower(key) {
|
||||
case GRPCBlockHeightHeader:
|
||||
return GRPCBlockHeightHeader, true
|
||||
|
||||
default:
|
||||
return runtime.DefaultHeaderMatcher(key)
|
||||
}
|
||||
}
|
||||
|
||||
// Register implements registers a grpc-gateway server
|
||||
func (s *Server) Register(r mux.Router) error {
|
||||
// configure grpc-gatway server
|
||||
if s.config.Enable {
|
||||
r.PathPrefix("/").Handler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
// Fall back to grpc gateway server.
|
||||
s.GRPCGatewayRouter.ServeHTTP(w, req)
|
||||
}))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package telemetry
|
||||
|
||||
type Config struct {
|
||||
// Prefixed with keys to separate services
|
||||
ServiceName string `mapstructure:"service-name" toml:"service-name" comment:"Prefixed with keys to separate services."`
|
||||
|
||||
// Enabled enables the application telemetry functionality. When enabled,
|
||||
// an in-memory sink is also enabled by default. Operators may also enabled
|
||||
// other sinks such as Prometheus.
|
||||
Enabled bool `mapstructure:"enabled" toml:"enabled" comment:"Enabled enables the application telemetry functionality. When enabled, an in-memory sink is also enabled by default. Operators may also enabled other sinks such as Prometheus."`
|
||||
|
||||
// Enable prefixing gauge values with hostname
|
||||
EnableHostname bool `mapstructure:"enable-hostname" toml:"enable-hostname" comment:"Enable prefixing gauge values with hostname."`
|
||||
|
||||
// Enable adding hostname to labels
|
||||
EnableHostnameLabel bool `mapstructure:"enable-hostname-label" toml:"enable-hostname-label" comment:"Enable adding hostname to labels."`
|
||||
|
||||
// Enable adding service to labels
|
||||
EnableServiceLabel bool `mapstructure:"enable-service-label" toml:"enable-service-label" comment:"Enable adding service to labels."`
|
||||
|
||||
// PrometheusRetentionTime, when positive, enables a Prometheus metrics sink.
|
||||
// It defines the retention duration in seconds.
|
||||
PrometheusRetentionTime int64 `mapstructure:"prometheus-retention-time" toml:"prometheus-retention-time" comment:"PrometheusRetentionTime, when positive, enables a Prometheus metrics sink. It defines the retention duration in seconds."`
|
||||
|
||||
// GlobalLabels defines a global set of name/value label tuples applied to all
|
||||
// metrics emitted using the wrapper functions defined in telemetry package.
|
||||
//
|
||||
// Example:
|
||||
// [["chain_id", "cosmoshub-1"]]
|
||||
GlobalLabels [][]string `mapstructure:"global-labels" toml:"global-labels" comment:"GlobalLabels defines a global set of name/value label tuples applied to all metrics emitted using the wrapper functions defined in telemetry package.\n Example:\n [[\"chain_id\", \"cosmoshub-1\"]]"`
|
||||
|
||||
// MetricsSink defines the type of metrics backend to use.
|
||||
MetricsSink string `mapstructure:"type" toml:"metrics-sink" comment:"MetricsSink defines the type of metrics backend to use. Default is in memory"`
|
||||
|
||||
// StatsdAddr defines the address of a statsd server to send metrics to.
|
||||
// Only utilized if MetricsSink is set to "statsd" or "dogstatsd".
|
||||
StatsdAddr string `mapstructure:"statsd-addr" toml:"stats-addr" comment:"StatsdAddr defines the address of a statsd server to send metrics to. Only utilized if MetricsSink is set to \"statsd\" or \"dogstatsd\"."`
|
||||
|
||||
// DatadogHostname defines the hostname to use when emitting metrics to
|
||||
// Datadog. Only utilized if MetricsSink is set to "dogstatsd".
|
||||
DatadogHostname string `mapstructure:"datadog-hostname" toml:"data-dog-hostname" comment:"DatadogHostname defines the hostname to use when emitting metrics to Datadog. Only utilized if MetricsSink is set to \"dogstatsd\"."`
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-metrics"
|
||||
"github.com/hashicorp/go-metrics/datadog"
|
||||
metricsprom "github.com/hashicorp/go-metrics/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/common/expfmt"
|
||||
)
|
||||
|
||||
// GlobalLabels defines the set of global labels that will be applied to all
|
||||
// metrics emitted using the telemetry package function wrappers.
|
||||
var GlobalLabels = []metrics.Label{} // nolint: ignore // false positive
|
||||
|
||||
// NewLabel creates a new instance of Label with name and value
|
||||
func NewLabel(name, value string) metrics.Label {
|
||||
return metrics.Label{Name: name, Value: value}
|
||||
}
|
||||
|
||||
// Metrics supported format types.
|
||||
const (
|
||||
FormatDefault = ""
|
||||
FormatPrometheus = "prometheus"
|
||||
FormatText = "text"
|
||||
ContentTypeText = `text/plain; version=` + expfmt.TextVersion + `; charset=utf-8`
|
||||
|
||||
MetricSinkInMem = "mem"
|
||||
MetricSinkStatsd = "statsd"
|
||||
MetricSinkDogsStatsd = "dogstatsd"
|
||||
)
|
||||
|
||||
// DisplayableSink is an interface that defines a method for displaying metrics.
|
||||
type DisplayableSink interface {
|
||||
DisplayMetrics(resp http.ResponseWriter, req *http.Request) (any, error)
|
||||
}
|
||||
|
||||
// Metrics defines a wrapper around application telemetry functionality. It allows
|
||||
// metrics to be gathered at any point in time. When creating a Metrics object,
|
||||
// internally, a global metrics is registered with a set of sinks as configured
|
||||
// by the operator. In addition to the sinks, when a process gets a SIGUSR1, a
|
||||
// dump of formatted recent metrics will be sent to STDERR.
|
||||
type Metrics struct {
|
||||
sink metrics.MetricSink
|
||||
prometheusEnabled bool
|
||||
}
|
||||
|
||||
// GatherResponse is the response type of registered metrics
|
||||
type GatherResponse struct {
|
||||
Metrics []byte
|
||||
ContentType string
|
||||
}
|
||||
|
||||
// New creates a new instance of Metrics
|
||||
func New(cfg Config) (_ *Metrics, rerr error) {
|
||||
if !cfg.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if numGlobalLabels := len(cfg.GlobalLabels); numGlobalLabels > 0 {
|
||||
parsedGlobalLabels := make([]metrics.Label, numGlobalLabels)
|
||||
for i, gl := range cfg.GlobalLabels {
|
||||
parsedGlobalLabels[i] = NewLabel(gl[0], gl[1])
|
||||
}
|
||||
GlobalLabels = parsedGlobalLabels
|
||||
}
|
||||
|
||||
metricsConf := metrics.DefaultConfig(cfg.ServiceName)
|
||||
metricsConf.EnableHostname = cfg.EnableHostname
|
||||
metricsConf.EnableHostnameLabel = cfg.EnableHostnameLabel
|
||||
|
||||
var (
|
||||
sink metrics.MetricSink
|
||||
err error
|
||||
)
|
||||
switch cfg.MetricsSink {
|
||||
case MetricSinkStatsd:
|
||||
sink, err = metrics.NewStatsdSink(cfg.StatsdAddr)
|
||||
case MetricSinkDogsStatsd:
|
||||
sink, err = datadog.NewDogStatsdSink(cfg.StatsdAddr, cfg.DatadogHostname)
|
||||
default:
|
||||
memSink := metrics.NewInmemSink(10*time.Second, time.Minute)
|
||||
sink = memSink
|
||||
inMemSig := metrics.DefaultInmemSignal(memSink)
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
inMemSig.Stop()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := &Metrics{sink: sink}
|
||||
fanout := metrics.FanoutSink{sink}
|
||||
|
||||
if cfg.PrometheusRetentionTime > 0 {
|
||||
m.prometheusEnabled = true
|
||||
prometheusOpts := metricsprom.PrometheusOpts{
|
||||
Expiration: time.Duration(cfg.PrometheusRetentionTime) * time.Second,
|
||||
}
|
||||
|
||||
promSink, err := metricsprom.NewPrometheusSinkFrom(prometheusOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fanout = append(fanout, promSink)
|
||||
}
|
||||
|
||||
if _, err := metrics.NewGlobal(metricsConf, fanout); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Gather collects all registered metrics and returns a GatherResponse where the
|
||||
// metrics are encoded depending on the type. Metrics are either encoded via
|
||||
// Prometheus or JSON if in-memory.
|
||||
func (m *Metrics) Gather(format string) (GatherResponse, error) {
|
||||
switch format {
|
||||
case FormatPrometheus:
|
||||
return m.gatherPrometheus()
|
||||
|
||||
case FormatText:
|
||||
return m.gatherGeneric()
|
||||
|
||||
case FormatDefault:
|
||||
return m.gatherGeneric()
|
||||
|
||||
default:
|
||||
return GatherResponse{}, fmt.Errorf("unsupported metrics format: %s", format)
|
||||
}
|
||||
}
|
||||
|
||||
// gatherPrometheus collects Prometheus metrics and returns a GatherResponse.
|
||||
// If Prometheus metrics are not enabled, it returns an error.
|
||||
func (m *Metrics) gatherPrometheus() (GatherResponse, error) {
|
||||
if !m.prometheusEnabled {
|
||||
return GatherResponse{}, fmt.Errorf("prometheus metrics are not enabled")
|
||||
}
|
||||
|
||||
metricsFamilies, err := prometheus.DefaultGatherer.Gather()
|
||||
if err != nil {
|
||||
return GatherResponse{}, fmt.Errorf("failed to gather prometheus metrics: %w", err)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
defer buf.Reset()
|
||||
|
||||
e := expfmt.NewEncoder(buf, expfmt.NewFormat(expfmt.TypeTextPlain))
|
||||
|
||||
for _, mf := range metricsFamilies {
|
||||
if err := e.Encode(mf); err != nil {
|
||||
return GatherResponse{}, fmt.Errorf("failed to encode prometheus metrics: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return GatherResponse{ContentType: ContentTypeText, Metrics: buf.Bytes()}, nil
|
||||
}
|
||||
|
||||
// gatherGeneric collects generic metrics and returns a GatherResponse.
|
||||
func (m *Metrics) gatherGeneric() (GatherResponse, error) {
|
||||
gm, ok := m.sink.(DisplayableSink)
|
||||
if !ok {
|
||||
return GatherResponse{}, fmt.Errorf("non in-memory metrics sink does not support generic format")
|
||||
}
|
||||
|
||||
summary, err := gm.DisplayMetrics(nil, nil)
|
||||
if err != nil {
|
||||
return GatherResponse{}, fmt.Errorf("failed to gather in-memory metrics: %w", err)
|
||||
}
|
||||
|
||||
content, err := json.Marshal(summary)
|
||||
if err != nil {
|
||||
return GatherResponse{}, fmt.Errorf("failed to encode in-memory metrics: %w", err)
|
||||
}
|
||||
|
||||
return GatherResponse{ContentType: "application/json", Metrics: content}, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func RegisterMetrics(r mux.Router, cfg Config) (*Metrics, error) {
|
||||
m, err := New(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metricsHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
format := strings.TrimSpace(r.FormValue("format"))
|
||||
|
||||
gr, err := m.Gather(format)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
bz, err := json.Marshal(errorResponse{Code: 400, Error: fmt.Sprintf("failed to gather metrics: %s", err)})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(bz)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", gr.ContentType)
|
||||
_, _ = w.Write(gr.Metrics)
|
||||
}
|
||||
|
||||
r.HandleFunc("/metrics", metricsHandler).Methods("GET")
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// errorResponse defines the attributes of a JSON error response.
|
||||
type errorResponse struct {
|
||||
Code int `json:"code,omitempty"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
Reference in New Issue
Block a user