chore: upstream more changes from v2 (#20387)
Co-authored-by: Matt Kocubinski <mkocubinski@gmail.com>
This commit is contained in:
parent
92dafe6dac
commit
d7cc6de7cc
71
proto/cosmos/streaming/v1/grpc.proto
Normal file
71
proto/cosmos/streaming/v1/grpc.proto
Normal file
@ -0,0 +1,71 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cosmos.streaming.v1;
|
||||
|
||||
option go_package = "cosmossdk.io/server/v2/streaming";
|
||||
|
||||
// ListenDeliverBlockRequest is the request type for the ListenDeliverBlock RPC method
|
||||
message ListenDeliverBlockRequest {
|
||||
int64 block_height = 1;
|
||||
repeated bytes txs = 2;
|
||||
repeated Event events = 3;
|
||||
repeated ExecTxResult tx_results = 4;
|
||||
}
|
||||
|
||||
// ListenDeliverBlockResponse is the response type for the ListenDeliverBlock RPC method
|
||||
message ListenDeliverBlockResponse {}
|
||||
|
||||
// ListenStateChangesRequest is the request type for the ListenStateChanges RPC method
|
||||
message ListenStateChangesRequest {
|
||||
int64 block_height = 1;
|
||||
repeated StoreKVPair change_set = 2;
|
||||
bytes app_hash = 3;
|
||||
}
|
||||
|
||||
// ListenStateChangesResponse is the response type for the ListenStateChanges RPC method
|
||||
message ListenStateChangesResponse {}
|
||||
|
||||
// ListenerService is the service for the Listener interface
|
||||
service ListenerService {
|
||||
// ListenDeliverBlock is the corresponding endpoint for Listener.ListenDeliverBlock
|
||||
rpc ListenDeliverBlock(ListenDeliverBlockRequest) returns (ListenDeliverBlockResponse);
|
||||
// ListenStateChanges is the corresponding endpoint for Listener.ListenStateChanges
|
||||
rpc ListenStateChanges(ListenStateChangesRequest) returns (ListenStateChangesResponse);
|
||||
}
|
||||
|
||||
// StoreKVPair is a single key-value pair, associated with a store.
|
||||
message StoreKVPair {
|
||||
// address defines the address of the account the state changes are coming from.
|
||||
// In case of modules you can expect a stringified
|
||||
bytes address = 1;
|
||||
// key defines the key of the address that changed.
|
||||
bytes key = 2;
|
||||
// value defines the value that changed, empty in case of removal.
|
||||
bytes value = 3;
|
||||
// delete defines if the key was removed.
|
||||
bool delete = 4; // true indicates a delete operation, false indicates a set operation
|
||||
}
|
||||
|
||||
// Event is a single event, associated with a transaction.
|
||||
message Event {
|
||||
string type = 1;
|
||||
repeated EventAttribute attributes = 2;
|
||||
}
|
||||
|
||||
// EventAttribute is a single key-value pair, associated with an event.
|
||||
message EventAttribute {
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
// ExecTxResult contains results of executing one individual transaction.
|
||||
message ExecTxResult {
|
||||
uint32 code = 1;
|
||||
bytes data = 2;
|
||||
string log = 3;
|
||||
string info = 4;
|
||||
int64 gas_wanted = 5;
|
||||
int64 gas_used = 6;
|
||||
repeated Event events = 7;
|
||||
string codespace = 8;
|
||||
}
|
||||
34
server/v2/api/grpc/config.go
Normal file
34
server/v2/api/grpc/config.go
Normal file
@ -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."`
|
||||
}
|
||||
5
server/v2/api/grpc/gogoreflection/doc.go
Normal file
5
server/v2/api/grpc/gogoreflection/doc.go
Normal file
@ -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
|
||||
76
server/v2/api/grpc/gogoreflection/fix_registration.go
Normal file
76
server/v2/api/grpc/gogoreflection/fix_registration.go
Normal file
@ -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
|
||||
}
|
||||
22
server/v2/api/grpc/gogoreflection/fix_registration_test.go
Normal file
22
server/v2/api/grpc/gogoreflection/fix_registration_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
478
server/v2/api/grpc/gogoreflection/serverreflection.go
Normal file
478
server/v2/api/grpc/gogoreflection/serverreflection.go
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
187
server/v2/api/grpc/server.go
Normal file
187
server/v2/api/grpc/server.go
Normal file
@ -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"
|
||||
}
|
||||
12
server/v2/api/grpcgateway/config.go
Normal file
12
server/v2/api/grpcgateway/config.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
82
server/v2/api/grpcgateway/server.go
Normal file
82
server/v2/api/grpcgateway/server.go
Normal file
@ -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
|
||||
}
|
||||
42
server/v2/api/telemetry/config.go
Normal file
42
server/v2/api/telemetry/config.go
Normal file
@ -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\"."`
|
||||
}
|
||||
188
server/v2/api/telemetry/metrics.go
Normal file
188
server/v2/api/telemetry/metrics.go
Normal file
@ -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
|
||||
}
|
||||
47
server/v2/api/telemetry/server.go
Normal file
47
server/v2/api/telemetry/server.go
Normal file
@ -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"`
|
||||
}
|
||||
76
server/v2/commands.go
Normal file
76
server/v2/commands.go
Normal file
@ -0,0 +1,76 @@
|
||||
package serverv2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"cosmossdk.io/log"
|
||||
)
|
||||
|
||||
func Commands(logger log.Logger, homePath string, modules ...ServerModule) (CLIConfig, error) {
|
||||
if len(modules) == 0 {
|
||||
// TODO figure if we should define default modules
|
||||
// and if so it should be done here to avoid uncessary dependencies
|
||||
return CLIConfig{}, errors.New("no modules provided")
|
||||
}
|
||||
|
||||
v, err := ReadConfig(filepath.Join(homePath, "config"))
|
||||
if err != nil {
|
||||
return CLIConfig{}, fmt.Errorf("failed to read config: %w", err)
|
||||
}
|
||||
|
||||
server := NewServer(logger, modules...)
|
||||
startCmd := &cobra.Command{
|
||||
Use: "start",
|
||||
Short: "Run the application",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := v.BindPFlags(cmd.Flags()); err != nil { // the server modules are already instantiated here, so binding the flags is useless.
|
||||
return err
|
||||
}
|
||||
|
||||
srvConfig := Config{StartBlock: true}
|
||||
ctx := cmd.Context()
|
||||
ctx = context.WithValue(ctx, ServerContextKey, srvConfig)
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
go func() {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
sig := <-sigCh
|
||||
cancelFn()
|
||||
cmd.Printf("caught %s signal\n", sig.String())
|
||||
|
||||
if err := server.Stop(ctx); err != nil {
|
||||
cmd.PrintErrln("failed to stop servers:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := server.Start(ctx); err != nil {
|
||||
return fmt.Errorf("failed to start servers: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmds := server.CLICommands()
|
||||
cmds.Commands = append(cmds.Commands, startCmd)
|
||||
|
||||
return cmds, nil
|
||||
}
|
||||
|
||||
func AddCommands(rootCmd *cobra.Command, logger log.Logger, homePath string, modules ...ServerModule) error {
|
||||
cmds, err := Commands(logger, homePath, modules...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rootCmd.AddCommand(cmds.Commands...)
|
||||
return nil
|
||||
}
|
||||
24
server/v2/config.go
Normal file
24
server/v2/config.go
Normal file
@ -0,0 +1,24 @@
|
||||
package serverv2
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
var ServerContextKey = struct{}{}
|
||||
|
||||
// Config is the config of the main server.
|
||||
type Config struct {
|
||||
// StartBlock indicates if the server should block or not.
|
||||
// If true, the server will block until the context is canceled.
|
||||
StartBlock bool
|
||||
}
|
||||
|
||||
// CLIConfig defines the CLI configuration for a module server.
|
||||
type CLIConfig struct {
|
||||
// Commands defines the main command of a module server.
|
||||
Commands []*cobra.Command
|
||||
// Queries defines the query commands of a module server.
|
||||
// Those commands are meant to be added in the root query command.
|
||||
Queries []*cobra.Command
|
||||
// Txs defines the tx commands of a module server.
|
||||
// Those commands are meant to be added in the root tx command.
|
||||
Txs []*cobra.Command
|
||||
}
|
||||
11
server/v2/flags.go
Normal file
11
server/v2/flags.go
Normal file
@ -0,0 +1,11 @@
|
||||
package serverv2
|
||||
|
||||
const (
|
||||
// Logging flags
|
||||
FlagLogLevel = "log_level"
|
||||
FlagLogFormat = "log_format"
|
||||
FlagLogNoColor = "log_no_color"
|
||||
FlagTrace = "trace"
|
||||
|
||||
OutputFormatJSON = "json"
|
||||
)
|
||||
85
server/v2/go.mod
Normal file
85
server/v2/go.mod
Normal file
@ -0,0 +1,85 @@
|
||||
module cosmossdk.io/server/v2
|
||||
|
||||
go 1.21
|
||||
|
||||
replace (
|
||||
cosmossdk.io/api => ../../api
|
||||
cosmossdk.io/core => ../../core
|
||||
cosmossdk.io/depinject => ../../depinject
|
||||
cosmossdk.io/server/v2 => .
|
||||
cosmossdk.io/server/v2/appmanager => ./appmanager
|
||||
cosmossdk.io/server/v2/stf => ./stf
|
||||
cosmossdk.io/x/tx => ../../x/tx
|
||||
)
|
||||
|
||||
require (
|
||||
cosmossdk.io/api v0.7.5
|
||||
cosmossdk.io/core v0.12.1-0.20231114100755-569e3ff6a0d7
|
||||
cosmossdk.io/log v1.3.1
|
||||
github.com/cosmos/cosmos-proto v1.0.0-beta.5
|
||||
github.com/cosmos/gogogateway v1.2.0
|
||||
github.com/cosmos/gogoproto v1.4.12
|
||||
github.com/golang/protobuf v1.5.4
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0
|
||||
github.com/hashicorp/go-hclog v1.6.2
|
||||
github.com/hashicorp/go-metrics v0.5.3
|
||||
github.com/hashicorp/go-plugin v1.6.0
|
||||
github.com/pelletier/go-toml/v2 v2.1.1
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/prometheus/common v0.52.2
|
||||
github.com/rs/zerolog v1.32.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/sync v0.7.0
|
||||
google.golang.org/grpc v1.64.0
|
||||
google.golang.org/protobuf v1.34.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gogo/googleapis v1.4.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.1 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jhump/protoreflect v1.15.3 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/procfs v0.13.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
393
server/v2/go.sum
Normal file
393
server/v2/go.sum
Normal file
@ -0,0 +1,393 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cosmossdk.io/log v1.3.1 h1:UZx8nWIkfbbNEWusZqzAx3ZGvu54TZacWib3EzUYmGI=
|
||||
cosmossdk.io/log v1.3.1/go.mod h1:2/dIomt8mKdk6vl3OWJcPk2be3pGOS8OQaLUM/3/tCM=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28/ZlunY=
|
||||
github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA=
|
||||
github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec=
|
||||
github.com/cosmos/gogogateway v1.2.0 h1:Ae/OivNhp8DqBi/sh2A8a1D0y638GpL3tkmLQAiKxTE=
|
||||
github.com/cosmos/gogogateway v1.2.0/go.mod h1:iQpLkGWxYcnCdz5iAdLcRBSw3h7NXeOkZ4GUkT+tbFI=
|
||||
github.com/cosmos/gogoproto v1.4.2/go.mod h1:cLxOsn1ljAHSV527CHOtaIP91kK6cCrZETRBrkzItWU=
|
||||
github.com/cosmos/gogoproto v1.4.12 h1:vB6Lbe/rtnYGjQuFxkPiPYiCybqFT8QvLipDZP8JpFE=
|
||||
github.com/cosmos/gogoproto v1.4.12/go.mod h1:LnZob1bXRdUoqMMtwYlcR3wjiElmlC+FkjaZRv1/eLY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/googleapis v1.4.1-0.20201022092350-68b0159b7869/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
|
||||
github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0=
|
||||
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I=
|
||||
github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-metrics v0.5.3 h1:M5uADWMOGCTUNU1YuC4hfknOeHNaX54LDm4oYSucoNE=
|
||||
github.com/hashicorp/go-metrics v0.5.3/go.mod h1:KEjodfebIOuBYSAe/bHTm+HChmKSxAOXPBieMLYozDE=
|
||||
github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A=
|
||||
github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jhump/protoreflect v1.15.3 h1:6SFRuqU45u9hIZPJAoZ8c28T3nK64BNdp9w6jFonzls=
|
||||
github.com/jhump/protoreflect v1.15.3/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4Elxc8qKP+p1k=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/common v0.52.2 h1:LW8Vk7BccEdONfrJBDffQGRtpSzi5CQaRZGtboOO2ck=
|
||||
github.com/prometheus/common v0.52.2/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
|
||||
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
|
||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 h1:mxSlqyb8ZAHsYDCfiXN1EDdNTdvjUJSLY+OnAUtYNYA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
||||
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
44
server/v2/logger.go
Normal file
44
server/v2/logger.go
Normal file
@ -0,0 +1,44 @@
|
||||
package serverv2
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"cosmossdk.io/log"
|
||||
)
|
||||
|
||||
// NewLogger creates a the default SDK logger.
|
||||
// It reads the log level and format from the server context.
|
||||
func NewLogger(v *viper.Viper, out io.Writer) (log.Logger, error) {
|
||||
var opts []log.Option
|
||||
if v.GetString(FlagLogFormat) == OutputFormatJSON {
|
||||
opts = append(opts, log.OutputJSONOption())
|
||||
}
|
||||
opts = append(opts,
|
||||
log.ColorOption(!v.GetBool(FlagLogNoColor)),
|
||||
log.TraceOption(v.GetBool(FlagTrace)))
|
||||
|
||||
// check and set filter level or keys for the logger if any
|
||||
logLvlStr := v.GetString(FlagLogLevel)
|
||||
if logLvlStr == "" {
|
||||
return log.NewLogger(out, opts...), nil
|
||||
}
|
||||
|
||||
logLvl, err := zerolog.ParseLevel(logLvlStr)
|
||||
switch {
|
||||
case err != nil:
|
||||
// If the log level is not a valid zerolog level, then we try to parse it as a key filter.
|
||||
filterFunc, err := log.ParseLogLevel(logLvlStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts = append(opts, log.FilterOption(filterFunc))
|
||||
default:
|
||||
opts = append(opts, log.LevelOption(logLvl))
|
||||
}
|
||||
|
||||
return log.NewLogger(out, opts...), nil
|
||||
}
|
||||
144
server/v2/server.go
Normal file
144
server/v2/server.go
Normal file
@ -0,0 +1,144 @@
|
||||
package serverv2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"cosmossdk.io/log"
|
||||
)
|
||||
|
||||
// ServerModule is a server module that can be started and stopped.
|
||||
type ServerModule interface {
|
||||
Name() string
|
||||
|
||||
Start(context.Context) error
|
||||
Stop(context.Context) error
|
||||
}
|
||||
|
||||
// HasCLICommands is a server module that has CLI commands.
|
||||
type HasCLICommands interface {
|
||||
CLICommands() CLIConfig
|
||||
}
|
||||
|
||||
// HasConfig is a server module that has a config.
|
||||
type HasConfig interface {
|
||||
Config() any
|
||||
}
|
||||
|
||||
var _ ServerModule = (*Server)(nil)
|
||||
|
||||
// Configs returns a viper instance of the config file
|
||||
func ReadConfig(configPath string) (*viper.Viper, error) {
|
||||
v := viper.New()
|
||||
v.SetConfigFile(configPath)
|
||||
v.SetConfigType("toml")
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read config: %s: %w", configPath, err)
|
||||
}
|
||||
v.WatchConfig()
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
logger log.Logger
|
||||
modules []ServerModule
|
||||
}
|
||||
|
||||
func NewServer(logger log.Logger, modules ...ServerModule) *Server {
|
||||
return &Server{
|
||||
logger: logger,
|
||||
modules: modules,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Name() string {
|
||||
return "server"
|
||||
}
|
||||
|
||||
// Start starts all modules concurrently.
|
||||
func (s *Server) Start(ctx context.Context) error {
|
||||
s.logger.Info("starting servers...")
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
for _, mod := range s.modules {
|
||||
mod := mod
|
||||
g.Go(func() error {
|
||||
return mod.Start(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return fmt.Errorf("failed to start servers: %w", err)
|
||||
}
|
||||
|
||||
serverCfg := ctx.Value(ServerContextKey).(Config)
|
||||
if serverCfg.StartBlock {
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops all modules concurrently.
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
s.logger.Info("stopping servers...")
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
for _, mod := range s.modules {
|
||||
mod := mod
|
||||
g.Go(func() error {
|
||||
return mod.Stop(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
// CLICommands returns all CLI commands of all modules.
|
||||
func (s *Server) CLICommands() CLIConfig {
|
||||
commands := CLIConfig{}
|
||||
for _, mod := range s.modules {
|
||||
if climod, ok := mod.(HasCLICommands); ok {
|
||||
commands.Commands = append(commands.Commands, climod.CLICommands().Commands...)
|
||||
commands.Queries = append(commands.Queries, climod.CLICommands().Queries...)
|
||||
commands.Txs = append(commands.Txs, climod.CLICommands().Txs...)
|
||||
}
|
||||
}
|
||||
|
||||
return commands
|
||||
}
|
||||
|
||||
// Configs returns all configs of all server modules.
|
||||
func (s *Server) Configs() map[string]any {
|
||||
cfgs := make(map[string]any)
|
||||
for _, mod := range s.modules {
|
||||
if configmod, ok := mod.(HasConfig); ok {
|
||||
cfg := configmod.Config()
|
||||
cfgs[mod.Name()] = cfg
|
||||
}
|
||||
}
|
||||
|
||||
return cfgs
|
||||
}
|
||||
|
||||
// WriteConfig writes the config to the given path.
|
||||
// Note: it does not use viper.WriteConfigAs because we do not want to store flag values in the config.
|
||||
func (s *Server) WriteConfig(configPath string) error {
|
||||
cfgs := s.Configs()
|
||||
b, err := toml.Marshal(cfgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, b, 0o600); err != nil {
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
50
server/v2/server_mock_test.go
Normal file
50
server/v2/server_mock_test.go
Normal file
@ -0,0 +1,50 @@
|
||||
package serverv2_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
type mockServerConfig struct {
|
||||
MockFieldOne string `mapstructure:"mock_field" toml:"mock_field" comment:"Mock field"`
|
||||
MockFieldTwo int `mapstructure:"mock_field_two" toml:"mock_field_two" comment:"Mock field two"`
|
||||
}
|
||||
|
||||
func MockServerDefaultConfig() *mockServerConfig {
|
||||
return &mockServerConfig{
|
||||
MockFieldOne: "default",
|
||||
MockFieldTwo: 1,
|
||||
}
|
||||
}
|
||||
|
||||
type mockServer struct {
|
||||
name string
|
||||
ch chan string
|
||||
}
|
||||
|
||||
func (s *mockServer) Name() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s *mockServer) Start(ctx context.Context) error {
|
||||
for ctx.Err() == nil {
|
||||
s.ch <- fmt.Sprintf("%s mock server: %d", s.name, rand.Int())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockServer) Stop(ctx context.Context) error {
|
||||
for range s.ch {
|
||||
if str := <-s.ch; str != "" {
|
||||
fmt.Printf("clearing %s\n", str)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockServer) Config() any {
|
||||
return MockServerDefaultConfig()
|
||||
}
|
||||
116
server/v2/server_test.go
Normal file
116
server/v2/server_test.go
Normal file
@ -0,0 +1,116 @@
|
||||
package serverv2_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gogogrpc "github.com/cosmos/gogoproto/grpc"
|
||||
gogoproto "github.com/cosmos/gogoproto/proto"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"cosmossdk.io/log"
|
||||
serverv2 "cosmossdk.io/server/v2"
|
||||
grpc "cosmossdk.io/server/v2/api/grpc"
|
||||
)
|
||||
|
||||
type mockGRPCService struct {
|
||||
grpc.GRPCService
|
||||
}
|
||||
|
||||
func (m *mockGRPCService) RegisterGRPCServer(gogogrpc.Server) {}
|
||||
|
||||
type mockInterfaceRegistry struct{}
|
||||
|
||||
func (*mockInterfaceRegistry) Resolve(typeUrl string) (gogoproto.Message, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*mockInterfaceRegistry) ListImplementations(ifaceTypeURL string) []string {
|
||||
panic("not implemented")
|
||||
}
|
||||
func (*mockInterfaceRegistry) ListAllInterfaces() []string { panic("not implemented") }
|
||||
|
||||
// TODO split this test into multiple tests
|
||||
// test read config
|
||||
// test write config
|
||||
// test server configs
|
||||
// test start empty
|
||||
// test start config exists
|
||||
// test stop
|
||||
func TestServer(t *testing.T) {
|
||||
currentDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.Fail()
|
||||
}
|
||||
configPath := filepath.Join(currentDir, "testdata", "app.toml")
|
||||
|
||||
v, err := serverv2.ReadConfig(configPath)
|
||||
if err != nil {
|
||||
v = viper.New()
|
||||
}
|
||||
|
||||
logger := log.NewLogger(os.Stdout)
|
||||
grpcServer, err := grpc.New(logger, v, &mockInterfaceRegistry{}, &mockGRPCService{})
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
mockServer := &mockServer{name: "mock-server-1", ch: make(chan string, 100)}
|
||||
|
||||
server := serverv2.NewServer(
|
||||
logger,
|
||||
grpcServer,
|
||||
mockServer,
|
||||
)
|
||||
|
||||
serverCfgs := server.Configs()
|
||||
if serverCfgs[grpcServer.Name()].(*grpc.Config).Address != grpc.DefaultConfig().Address {
|
||||
t.Logf("config is not equal: %v", serverCfgs[grpcServer.Name()])
|
||||
t.Fail()
|
||||
}
|
||||
if serverCfgs[mockServer.Name()].(*mockServerConfig).MockFieldOne != MockServerDefaultConfig().MockFieldOne {
|
||||
t.Logf("config is not equal: %v", serverCfgs[mockServer.Name()])
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
// write config
|
||||
if err := server.WriteConfig(configPath); err != nil {
|
||||
t.Log(err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
v, err = serverv2.ReadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Log(err) // config should be created by WriteConfig
|
||||
t.FailNow()
|
||||
}
|
||||
if v.GetString(grpcServer.Name()+".address") != grpc.DefaultConfig().Address {
|
||||
t.Logf("config is not equal: %v", v)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
// start empty
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, serverv2.ServerContextKey, serverv2.Config{StartBlock: true})
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
go func() {
|
||||
// wait 5sec and cancel context
|
||||
<-time.After(5 * time.Second)
|
||||
cancelFn()
|
||||
|
||||
if err := server.Stop(ctx); err != nil {
|
||||
t.Logf("failed to stop servers: %s", err)
|
||||
t.Fail()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := server.Start(ctx); err != nil {
|
||||
t.Log(err)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
30
server/v2/streaming/README.md
Normal file
30
server/v2/streaming/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Cosmos-SDK Plugins
|
||||
|
||||
This package contains an extensible plugin system for the Cosmos-SDK. The plugin system leverages the [hashicorp/go-plugin](https://github.com/hashicorp/go-plugin) system. This system is designed to work over RPC.
|
||||
|
||||
Although the `go-plugin` is built to work over RPC, it is currently only designed to work over a local network.
|
||||
|
||||
## Pre requisites
|
||||
|
||||
For an overview of supported features by the `go-plugin` system, please see https://github.com/hashicorp/go-plugin. The `go-plugin` documentation is located [here](https://github.com/hashicorp/go-plugin/tree/master/docs). You can also directly visit any of the links below:
|
||||
|
||||
* [Writing plugins without Go](https://github.com/hashicorp/go-plugin/blob/master/docs/guide-plugin-write-non-go.md)
|
||||
* [Go Plugin Tutorial](https://github.com/hashicorp/go-plugin/blob/master/docs/extensive-go-plugin-tutorial.md)
|
||||
* [Plugin Internals](https://github.com/hashicorp/go-plugin/blob/master/docs/internals.md)
|
||||
* [Plugin Architecture](https://www.youtube.com/watch?v=SRvm3zQQc1Q) (start here)
|
||||
|
||||
## Exposing plugins
|
||||
|
||||
To expose plugins to the plugin system, you will need to:
|
||||
|
||||
1. Implement the gRPC message protocol service of the plugin
|
||||
2. Build the plugin binary
|
||||
3. Export it
|
||||
|
||||
Read the plugin documentation in the [Streaming Plugins](#streaming-plugins) section for examples on how to build a plugin.
|
||||
|
||||
## Streaming Plugins
|
||||
|
||||
List of support streaming plugins
|
||||
|
||||
* [State Streaming Plugin](plugin.md)
|
||||
25
server/v2/streaming/config.go
Normal file
25
server/v2/streaming/config.go
Normal file
@ -0,0 +1,25 @@
|
||||
package streaming
|
||||
|
||||
// State Streaming configuration
|
||||
|
||||
// StreamingConfig defines application configuration for external streaming services
|
||||
type StreamingConfig struct {
|
||||
ListenerConfig ListenerConfig `mapstructure:"listener-config" toml:"listener-config" comment:"ListenerConfig defines application configuration for ABCIListener streaming service"`
|
||||
}
|
||||
|
||||
// ListenerConfig defines application configuration for ABCIListener streaming service
|
||||
type ListenerConfig struct {
|
||||
// List of kv store keys to stream out via gRPC.
|
||||
// The store key names MUST match the module's StoreKey name.
|
||||
//
|
||||
// Example:
|
||||
// ["acc", "bank", "gov", "staking", "mint"[,...]]
|
||||
// ["*"] to expose all keys.
|
||||
Keys []string `mapstructure:"keys" toml:"keys" comment:"List of kv store keys to stream out via gRPC. The store key names MUST match the module's StoreKey name. Example: [\"acc\", \"bank\", \"gov\", \"staking\", \"mint\"[,...]] [\"*\"] to expose all keys."`
|
||||
// The plugin name used for streaming via gRPC.
|
||||
// Streaming is only enabled if this is set.
|
||||
// Supported plugins: abci
|
||||
Plugin string `mapstructure:"plugin" toml:"plugin" comment:"The plugin name used for streaming via gRPC. Streaming is only enabled if this is set. Supported plugins: abci"`
|
||||
// stop-node-on-err specifies whether to stop the node on message delivery error.
|
||||
StopNodeOnErr bool `mapstructure:"stop-node-on-err" toml:"stop-node-on-err" comment:"stop-node-on-err specifies whether to stop the node on message delivery error."`
|
||||
}
|
||||
11
server/v2/streaming/context.go
Normal file
11
server/v2/streaming/context.go
Normal file
@ -0,0 +1,11 @@
|
||||
package streaming
|
||||
|
||||
import "cosmossdk.io/log"
|
||||
|
||||
// Context is an interface used by an App to pass context information
|
||||
// needed to process store streaming requests.
|
||||
type Context interface {
|
||||
BlockHeight() int64
|
||||
Logger() log.Logger
|
||||
StreamingManager() Manager
|
||||
}
|
||||
2
server/v2/streaming/examples/file/.gitignore
vendored
Normal file
2
server/v2/streaming/examples/file/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# ignore the file plugin binary
|
||||
file
|
||||
17
server/v2/streaming/examples/file/README.md
Normal file
17
server/v2/streaming/examples/file/README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# File Plugin
|
||||
|
||||
The file plugin is an example plugin written in Go. It is intended for local testing and should not be used in production environments.
|
||||
|
||||
## Build
|
||||
|
||||
To build the plugin run the following command:
|
||||
|
||||
```shell
|
||||
cd store
|
||||
```
|
||||
|
||||
```shell
|
||||
go build -o streaming/abci/examples/file/file streaming/abci/examples/file/file.go
|
||||
```
|
||||
|
||||
* The plugin will write files to the users home directory `~/`.
|
||||
78
server/v2/streaming/examples/file/file.go
Normal file
78
server/v2/streaming/examples/file/file.go
Normal file
@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"cosmossdk.io/server/v2/streaming"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
)
|
||||
|
||||
// FilePlugin is the implementation of the baseapp.ABCIListener interface
|
||||
// For Go plugins this is all that is required to process data sent over gRPC.
|
||||
type FilePlugin struct {
|
||||
BlockHeight int64
|
||||
}
|
||||
|
||||
func (a *FilePlugin) writeToFile(file string, data []byte) error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("%s/%s.txt", home, file)
|
||||
f, err := os.OpenFile(filepath.Clean(filename), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := f.Write(data); err != nil {
|
||||
f.Close() // ignore error; Write error takes precedence
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *FilePlugin) ListenDeliverBlock(ctx context.Context, req streaming.ListenDeliverBlockRequest) error {
|
||||
d1 := []byte(fmt.Sprintf("%d:::%v\n", a.BlockHeight, req))
|
||||
d2 := []byte(fmt.Sprintf("%d:::%v\n", a.BlockHeight, req))
|
||||
if err := a.writeToFile("finalize-block-req", d1); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.writeToFile("finalize-block-res", d2); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *FilePlugin) ListenStateChanges(ctx context.Context, changeSet []*streaming.StoreKVPair) error {
|
||||
fmt.Printf("listen-commit: block_height=%d data=%v", a.BlockHeight, changeSet)
|
||||
d1 := []byte(fmt.Sprintf("%d:::%v\n", a.BlockHeight, nil))
|
||||
d2 := []byte(fmt.Sprintf("%d:::%v\n", a.BlockHeight, changeSet))
|
||||
if err := a.writeToFile("commit-res", d1); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.writeToFile("state-change", d2); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
plugin.Serve(&plugin.ServeConfig{
|
||||
HandshakeConfig: streaming.Handshake,
|
||||
Plugins: map[string]plugin.Plugin{
|
||||
"abci": &streaming.ListenerGRPCPlugin{Impl: &FilePlugin{}},
|
||||
},
|
||||
|
||||
// A non-nil value here enables gRPC serving for this streaming...
|
||||
GRPCServer: plugin.DefaultGRPCServer,
|
||||
})
|
||||
}
|
||||
BIN
server/v2/streaming/examples/stdout/stdout
Executable file
BIN
server/v2/streaming/examples/stdout/stdout
Executable file
Binary file not shown.
41
server/v2/streaming/examples/stdout/stdout.go
Normal file
41
server/v2/streaming/examples/stdout/stdout.go
Normal file
@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-plugin"
|
||||
|
||||
"cosmossdk.io/server/v2/streaming"
|
||||
)
|
||||
|
||||
// StdoutPlugin is the implementation of the ABCIListener interface
|
||||
// For Go plugins this is all that is required to process data sent over gRPC.
|
||||
type StdoutPlugin struct {
|
||||
BlockHeight int64
|
||||
}
|
||||
|
||||
func (a *StdoutPlugin) ListenDeliverBlock(ctx context.Context, req streaming.ListenDeliverBlockRequest) error {
|
||||
a.BlockHeight = req.BlockHeight
|
||||
// process tx messages (i.e: sent to external system)
|
||||
fmt.Printf("listen-finalize-block: block-height=%d req=%v res=%v", a.BlockHeight, req, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *StdoutPlugin) ListenStateChanges(ctx context.Context, changeSet []*streaming.StoreKVPair) error {
|
||||
// process block commit messages (i.e: sent to external system)
|
||||
fmt.Printf("listen-commit: block_height=%d res=%v data=%v", a.BlockHeight, changeSet, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
plugin.Serve(&plugin.ServeConfig{
|
||||
HandshakeConfig: streaming.Handshake,
|
||||
Plugins: map[string]plugin.Plugin{
|
||||
"abci": &streaming.ListenerGRPCPlugin{Impl: &StdoutPlugin{}},
|
||||
},
|
||||
|
||||
// A non-nil value here enables gRPC serving for this streaming...
|
||||
GRPCServer: plugin.DefaultGRPCServer,
|
||||
})
|
||||
}
|
||||
75
server/v2/streaming/grpc.go
Normal file
75
server/v2/streaming/grpc.go
Normal file
@ -0,0 +1,75 @@
|
||||
package streaming
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/go-plugin"
|
||||
)
|
||||
|
||||
var _ Listener = (*GRPCClient)(nil)
|
||||
|
||||
// GRPCClient is an implementation of the ABCIListener interface that talks over RPC.
|
||||
type GRPCClient struct {
|
||||
client ListenerServiceClient
|
||||
}
|
||||
|
||||
// ListenEndBlock listens to end block request and responses.
|
||||
// In addition, it retrieves a types.Context from a context.Context instance.
|
||||
// It panics if a types.Context was not properly attached.
|
||||
// When the node is configured to stop on listening errors,
|
||||
// it will terminate immediately and exit with a non-zero code.
|
||||
func (m *GRPCClient) ListenDeliverBlock(goCtx context.Context, req ListenDeliverBlockRequest) error {
|
||||
ctx := goCtx.(Context)
|
||||
sm := ctx.StreamingManager()
|
||||
_, err := m.client.ListenDeliverBlock(goCtx, &req)
|
||||
if err != nil && sm.StopNodeOnErr {
|
||||
ctx.Logger().Error("DeliverBLock listening hook failed", "height", ctx.BlockHeight(), "err", err)
|
||||
cleanupAndExit()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ListenCommit listens to commit responses and state changes for the current block.
|
||||
// In addition, it retrieves a types.Context from a context.Context instance.
|
||||
// It panics if a types.Context was not properly attached.
|
||||
// When the node is configured to stop on listening errors,
|
||||
// it will terminate immediately and exit with a non-zero code.
|
||||
func (m *GRPCClient) ListenStateChanges(goCtx context.Context, changeSet []*StoreKVPair) error {
|
||||
ctx := goCtx.(Context)
|
||||
sm := ctx.StreamingManager()
|
||||
request := &ListenStateChangesRequest{BlockHeight: ctx.BlockHeight(), ChangeSet: changeSet}
|
||||
_, err := m.client.ListenStateChanges(goCtx, request)
|
||||
if err != nil && sm.StopNodeOnErr {
|
||||
ctx.Logger().Error("Commit listening hook failed", "height", ctx.BlockHeight(), "err", err)
|
||||
cleanupAndExit()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func cleanupAndExit() {
|
||||
plugin.CleanupClients()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var _ ListenerServiceServer = (*GRPCServer)(nil)
|
||||
|
||||
// GRPCServer is the gRPC server that GRPCClient talks to.
|
||||
type GRPCServer struct {
|
||||
// This is the real implementation
|
||||
Impl Listener
|
||||
}
|
||||
|
||||
func (m GRPCServer) ListenDeliverBlock(ctx context.Context, request *ListenDeliverBlockRequest) (*ListenDeliverBlockResponse, error) {
|
||||
if err := m.Impl.ListenDeliverBlock(ctx, *request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ListenDeliverBlockResponse{}, nil
|
||||
}
|
||||
|
||||
func (m GRPCServer) ListenStateChanges(ctx context.Context, request *ListenStateChangesRequest) (*ListenStateChangesResponse, error) {
|
||||
if err := m.Impl.ListenStateChanges(ctx, request.ChangeSet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ListenStateChangesResponse{}, nil
|
||||
}
|
||||
2414
server/v2/streaming/grpc.pb.go
Normal file
2414
server/v2/streaming/grpc.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
54
server/v2/streaming/interface.go
Normal file
54
server/v2/streaming/interface.go
Normal file
@ -0,0 +1,54 @@
|
||||
// Package abci contains shared data between the host and plugins.
|
||||
package streaming
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Listener is the interface that we're exposing as a streaming service.
|
||||
// It hooks into the ABCI message processing of the BaseApp.
|
||||
// The error results are propagated to consensus state machine,
|
||||
// if you don't want to affect consensus, handle the errors internally and always return `nil` in these APIs.
|
||||
type Listener interface {
|
||||
// ListenDeliverBlock updates the streaming service with the latest Delivered Block messages
|
||||
ListenDeliverBlock(context.Context, ListenDeliverBlockRequest) error
|
||||
// ListenCommit updates the steaming service with the latest Commit messages and state changes
|
||||
ListenStateChanges(ctx context.Context, changeSet []*StoreKVPair) error
|
||||
}
|
||||
|
||||
// Handshake is a common handshake that is shared by streaming and host.
|
||||
// This prevents users from executing bad plugins or executing a plugin
|
||||
// directory. It is a UX feature, not a security feature.
|
||||
var Handshake = plugin.HandshakeConfig{
|
||||
// This isn't required when using VersionedPlugins
|
||||
ProtocolVersion: 1,
|
||||
MagicCookieKey: "ABCI_LISTENER_PLUGIN",
|
||||
MagicCookieValue: "ef78114d-7bdf-411c-868f-347c99a78345",
|
||||
}
|
||||
|
||||
var _ plugin.GRPCPlugin = (*ListenerGRPCPlugin)(nil)
|
||||
|
||||
// ListenerGRPCPlugin is the implementation of plugin.GRPCPlugin, so we can serve/consume this.
|
||||
type ListenerGRPCPlugin struct {
|
||||
// GRPCPlugin must still implement the Plugin interface
|
||||
plugin.Plugin
|
||||
// Concrete implementation, written in Go. This is only used for plugins
|
||||
// that are written in Go.
|
||||
Impl Listener
|
||||
}
|
||||
|
||||
func (p *ListenerGRPCPlugin) GRPCServer(_ *plugin.GRPCBroker, s *grpc.Server) error {
|
||||
RegisterListenerServiceServer(s, &GRPCServer{Impl: p.Impl})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ListenerGRPCPlugin) GRPCClient(
|
||||
_ context.Context,
|
||||
_ *plugin.GRPCBroker,
|
||||
c *grpc.ClientConn,
|
||||
) (interface{}, error) {
|
||||
return &GRPCClient{client: NewListenerServiceClient(c)}, nil
|
||||
}
|
||||
212
server/v2/streaming/plugin.md
Normal file
212
server/v2/streaming/plugin.md
Normal file
@ -0,0 +1,212 @@
|
||||
# State Streaming Plugin (gRPC)
|
||||
|
||||
<!-- TODO fix docs before final release -->
|
||||
|
||||
The `Server/v2` package contains the interface for a [Listener](https://github.com/cosmos/cosmos-sdk/blob/main/baseapp/streaming.go)
|
||||
service used to write state changes out from individual KVStores to external systems,
|
||||
as described in [ADR-038](https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-038-state-listening.md).
|
||||
|
||||
Specific `ABCIListener` service implementations are written and loaded as [hashicorp/go-plugin](https://github.com/hashicorp/go-plugin).
|
||||
|
||||
## Implementation
|
||||
|
||||
In this section we describe the implementation of the `ABCIListener` interface as a gRPC service.
|
||||
|
||||
### Service Protocol
|
||||
|
||||
The companion service protocol for the `Listener` interface is described below.
|
||||
See [proto/cosmos/store/streaming/abci/grpc.proto](https://github.com/cosmos/cosmos-sdk/blob/main/proto/cosmos/store/streaming/abci/grpc.proto) for full details.
|
||||
|
||||
```protobuf reference
|
||||
https://github.com/cosmos/cosmos-sdk/blob/6cee22df52eb0cbb30e351fbb41f66d26c1f8300/proto/cosmos/store/streaming/abci/grpc.proto#L1-L36
|
||||
```
|
||||
|
||||
### Generating the Code
|
||||
|
||||
To generate the stubs the local client implementation can call, run the following command:
|
||||
|
||||
```shell
|
||||
make proto-gen
|
||||
```
|
||||
|
||||
For other languages you'll need to [download](https://github.com/cosmos/cosmos-sdk/blob/main/third_party/proto/README.md)
|
||||
the CosmosSDK protos into your project and compile. For language specific compilation instructions visit
|
||||
[https://github.com/grpc](https://github.com/grpc) and look in the `examples` folder of your
|
||||
language of choice `https://github.com/grpc/grpc-{language}/tree/master/examples` and [https://grpc.io](https://grpc.io)
|
||||
for the documentation.
|
||||
|
||||
### gRPC Client and Server
|
||||
|
||||
Implementing the ABCIListener gRPC client and server is a simple and straight forward process.
|
||||
|
||||
To create the client and server we create a `ListenerGRPCPlugin` struct that implements the
|
||||
`plugin.GRPCPlugin` interface and a `Impl` property that will contain a concrete implementation
|
||||
of the `ABCIListener` plugin written in Go.
|
||||
|
||||
#### The Interface
|
||||
|
||||
The `Server/v2` `Listener` interface will be what will define the plugins capabilities.
|
||||
|
||||
Boilerplate RPC implementation example of the `Listener` interface. ([store/streaming/abci/grpc.go](https://github.com/cosmos/cosmos-sdk/blob/main/store/streaming/abci/grpc.go))
|
||||
|
||||
```go reference
|
||||
https://github.com/cosmos/cosmos-sdk/blob/f851e188b3b9d46e7c63fa514ad137e6d558fdd9/store/streaming/abci/grpc.go#L13-L79
|
||||
```
|
||||
|
||||
Our `listener` service plugin. ([store/streaming/plugins/abci/v1/interface.go](interface.go))
|
||||
|
||||
```go reference
|
||||
https://github.com/cosmos/cosmos-sdk/blob/f851e188b3b9d46e7c63fa514ad137e6d558fdd9/store/streaming/abci/interface.go#L13-L45
|
||||
```
|
||||
|
||||
#### Plugin Implementation
|
||||
|
||||
Plugin implementations can be in a completely separate package but will need access
|
||||
to the `Listener` interface. One thing to note here is that plugin implementations
|
||||
defined in the `ListenerGRPCPlugin.Impl` property are **only** required when building
|
||||
plugins in Go. They are pre-compiled into Go modules. The `GRPCServer.Impl` calls methods
|
||||
on this out-of-process plugin.
|
||||
|
||||
For Go plugins this is all that is required to process data that is sent over gRPC.
|
||||
This provides the advantage of writing quick plugins that process data to different
|
||||
external systems (i.e: DB, File, DB, Kafka, etc.) without the need for implementing
|
||||
the gRPC server endpoints.
|
||||
|
||||
```go
|
||||
// MyPlugin is the implementation of the ABCIListener interface
|
||||
// For Go plugins this is all that is required to process data sent over gRPC.
|
||||
type MyPlugin struct {
|
||||
...
|
||||
}
|
||||
|
||||
func (a FilePlugin) ListenFinalizeBlock(ctx context.Context, req abci.RequestBeginBlock, res abci.ResponseBeginBlock) error {
|
||||
// process data
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a FilePlugin) ListenCommit(ctx context.Context, res abci.ResponseCommit, changeSet []*store.StoreKVPair) error {
|
||||
// process data
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
plugin.Serve(&plugin.ServeConfig{
|
||||
HandshakeConfig: v1.Handshake,
|
||||
Plugins: map[string]plugin.Plugin{
|
||||
"abci": &ABCIListenerGRPCPlugin{Impl: &MyPlugin{}},
|
||||
},
|
||||
|
||||
// A non-nil value here enables gRPC serving for this streaming...
|
||||
GRPCServer: plugin.DefaultGRPCServer,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Loading System
|
||||
|
||||
A general purpose plugin loading system has been provided by the SDK to be able to load not just
|
||||
the `ABCIListener` service plugin but other protocol services as well. You can take a look
|
||||
at how plugins are loaded by the SDK in [store/streaming/streaming.go](https://github.com/cosmos/cosmos-sdk/blob/main/store/streaming/streaming.go)
|
||||
|
||||
You'll need to add this in your `app.go`
|
||||
|
||||
```go
|
||||
// app.go
|
||||
|
||||
func NewApp(...) *App {
|
||||
|
||||
...
|
||||
|
||||
// register streaming services
|
||||
streamingCfg := cast.ToStringMap(appOpts.Get(baseapp.StreamingTomlKey))
|
||||
for service := range streamingCfg {
|
||||
pluginKey := fmt.Sprintf("%s.%s.%s", baseapp.StreamingTomlKey, service, baseapp.StreamingABCIPluginTomlKey)
|
||||
pluginName := strings.TrimSpace(cast.ToString(appOpts.Get(pluginKey)))
|
||||
if len(pluginName) > 0 {
|
||||
logLevel := cast.ToString(appOpts.Get(flags.FlagLogLevel))
|
||||
plugin, err := streaming.NewStreamingPlugin(pluginName, logLevel)
|
||||
if err != nil {
|
||||
tmos.Exit(err.Error())
|
||||
}
|
||||
if err := baseapp.RegisterStreamingPlugin(bApp, appOpts, keys, plugin); err != nil {
|
||||
tmos.Exit(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Update the streaming section in `app.toml`
|
||||
|
||||
```toml
|
||||
# Streaming allows nodes to stream state to external systems
|
||||
[streaming]
|
||||
|
||||
# streaming.abci specifies the configuration for the ABCI Listener streaming service
|
||||
[streaming.abci]
|
||||
|
||||
# List of kv store keys to stream out via gRPC
|
||||
# Set to ["*"] to expose all keys.
|
||||
keys = ["*"]
|
||||
|
||||
# The plugin name used for streaming via gRPC
|
||||
# Supported plugins: abci
|
||||
plugin = "abci"
|
||||
|
||||
# stop-node-on-err specifies whether to stop the node when the
|
||||
stop-node-on-err = true
|
||||
```
|
||||
|
||||
## Updating the protocol
|
||||
|
||||
If you update the protocol buffers file, you can regenerate the file and plugins using the
|
||||
following commands from the project root directory. You do not need to run this if you're
|
||||
just trying the examples, you can skip ahead to the [Testing](#testing) section.
|
||||
|
||||
```shell
|
||||
make proto-gen
|
||||
```
|
||||
|
||||
* stdout plugin; from inside the `store/` dir, run:
|
||||
|
||||
```shell
|
||||
go build -o streaming/abci/examples/stdout/stdout streaming/abci/examples/stdout/stdout.go
|
||||
```
|
||||
|
||||
* file plugin (writes to `~/`); from inside the `store/` dir, run:
|
||||
|
||||
```shell
|
||||
go build -o streaming/abci/examples/file/file streaming/abci/examples/file/file.go
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
Export a plugin from one of the Go or Python examples.
|
||||
|
||||
* stdout plugin
|
||||
|
||||
```shell
|
||||
export COSMOS_SDK_ABCI="{path to}/cosmos-sdk/store/streaming/abci/examples/stdout/stdout"
|
||||
```
|
||||
|
||||
* file plugin (writes to ~/)
|
||||
|
||||
```shell
|
||||
export COSMOS_SDK_ABCI="{path to}/cosmos-sdk/store/streaming/abci/examples/file/file"
|
||||
```
|
||||
|
||||
where `{path to}` is the parent path to the `cosmos-sdk` repo on you system.
|
||||
|
||||
Test:
|
||||
|
||||
```shell
|
||||
make test-sim-nondeterminism-streaming
|
||||
```
|
||||
|
||||
The plugin system will look for the plugin binary in the `env` variable `COSMOS_SDK_{PLUGIN_NAME}` above
|
||||
and if it does not find it, it will error out. The plugin UPPERCASE name is that of the
|
||||
`streaming.abci.plugin` TOML configuration setting.
|
||||
80
server/v2/streaming/streaming.go
Normal file
80
server/v2/streaming/streaming.go
Normal file
@ -0,0 +1,80 @@
|
||||
package streaming
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
pluginEnvKeyPrefix = "COSMOS_SDK"
|
||||
defaultPlugin = "grpc"
|
||||
)
|
||||
|
||||
// HandshakeMap contains a map of each supported streaming's handshake config
|
||||
var HandshakeMap = map[string]plugin.HandshakeConfig{
|
||||
defaultPlugin: Handshake,
|
||||
}
|
||||
|
||||
// PluginMap contains a map of supported gRPC plugins
|
||||
var PluginMap = map[string]plugin.Plugin{
|
||||
defaultPlugin: &ListenerGRPCPlugin{},
|
||||
}
|
||||
|
||||
func GetPluginEnvKey(name string) string {
|
||||
return fmt.Sprintf("%s_%s", pluginEnvKeyPrefix, strings.ToUpper(name))
|
||||
}
|
||||
|
||||
func NewStreamingPlugin(name, logLevel string) (interface{}, error) {
|
||||
logger := hclog.New(&hclog.LoggerOptions{
|
||||
Output: hclog.DefaultOutput,
|
||||
Level: toHclogLevel(logLevel),
|
||||
Name: fmt.Sprintf("plugin.%s", name),
|
||||
})
|
||||
|
||||
// We're a host. Start by launching the streaming process.
|
||||
env := os.Getenv(GetPluginEnvKey(name))
|
||||
client := plugin.NewClient(&plugin.ClientConfig{
|
||||
HandshakeConfig: HandshakeMap[name],
|
||||
Managed: true,
|
||||
Plugins: PluginMap,
|
||||
// For verifying the integrity of executables see SecureConfig documentation
|
||||
// https://pkg.go.dev/github.com/hashicorp/go-plugin#SecureConfig
|
||||
//#nosec G204 -- Required to load plugins
|
||||
Cmd: exec.Command("sh", "-c", env),
|
||||
Logger: logger,
|
||||
AllowedProtocols: []plugin.Protocol{
|
||||
plugin.ProtocolNetRPC, plugin.ProtocolGRPC,
|
||||
},
|
||||
})
|
||||
|
||||
// Connect via RPC
|
||||
rpcClient, err := client.Client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Request streaming plugin
|
||||
return rpcClient.Dispense(name)
|
||||
}
|
||||
|
||||
func toHclogLevel(s string) hclog.Level {
|
||||
switch s {
|
||||
case "trace":
|
||||
return hclog.Trace
|
||||
case "debug":
|
||||
return hclog.Debug
|
||||
case "info":
|
||||
return hclog.Info
|
||||
case "warn":
|
||||
return hclog.Warn
|
||||
case "error":
|
||||
return hclog.Error
|
||||
default:
|
||||
return hclog.DefaultLevel
|
||||
}
|
||||
}
|
||||
147
server/v2/streaming/streaming_test.go
Normal file
147
server/v2/streaming/streaming_test.go
Normal file
@ -0,0 +1,147 @@
|
||||
package streaming
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"cosmossdk.io/log"
|
||||
)
|
||||
|
||||
type PluginTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
loggerCtx MockContext
|
||||
|
||||
workDir string
|
||||
|
||||
deliverBlockrequest ListenDeliverBlockRequest
|
||||
stateChangeRequest ListenStateChangesRequest
|
||||
|
||||
changeSet []*StoreKVPair
|
||||
}
|
||||
|
||||
func (s *PluginTestSuite) SetupTest() {
|
||||
if runtime.GOOS != "linux" {
|
||||
s.T().Skip("only run on linux")
|
||||
}
|
||||
|
||||
path, err := os.Getwd()
|
||||
if err != nil {
|
||||
s.T().Fail()
|
||||
}
|
||||
s.workDir = path
|
||||
|
||||
pluginVersion := defaultPlugin
|
||||
// to write data to files, replace stdout/stdout => file/file
|
||||
pluginPath := fmt.Sprintf("%s/abci/examples/stdout/stdout", s.workDir)
|
||||
if err := os.Setenv(GetPluginEnvKey(pluginVersion), pluginPath); err != nil {
|
||||
s.T().Fail()
|
||||
}
|
||||
|
||||
raw, err := NewStreamingPlugin(pluginVersion, "trace")
|
||||
require.NoError(s.T(), err, "load", "streaming", "unexpected error")
|
||||
|
||||
abciListener, ok := raw.(Listener)
|
||||
require.True(s.T(), ok, "should pass type check")
|
||||
|
||||
logger := log.NewNopLogger()
|
||||
streamingService := Manager{
|
||||
Listeners: []Listener{abciListener},
|
||||
StopNodeOnErr: true,
|
||||
}
|
||||
s.loggerCtx = NewMockContext(1, logger, streamingService)
|
||||
|
||||
// test abci message types
|
||||
|
||||
s.deliverBlockrequest = ListenDeliverBlockRequest{
|
||||
BlockHeight: s.loggerCtx.BlockHeight(),
|
||||
Txs: [][]byte{{1, 2, 3, 4, 5, 6, 7, 8, 9}},
|
||||
Events: []*Event{},
|
||||
}
|
||||
s.stateChangeRequest = ListenStateChangesRequest{}
|
||||
|
||||
key := []byte("mockStore")
|
||||
key = append(key, 1, 2, 3)
|
||||
// test store kv pair types
|
||||
for range [2000]int{} {
|
||||
s.changeSet = append(s.changeSet, &StoreKVPair{
|
||||
Key: key,
|
||||
Value: []byte{3, 2, 1},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(PluginTestSuite))
|
||||
}
|
||||
|
||||
func (s *PluginTestSuite) TestABCIGRPCPlugin() {
|
||||
s.T().Run("Should successfully load streaming", func(t *testing.T) {
|
||||
abciListeners := s.loggerCtx.StreamingManager().Listeners
|
||||
for _, abciListener := range abciListeners {
|
||||
for i := range [50]int{} {
|
||||
|
||||
err := abciListener.ListenDeliverBlock(s.loggerCtx, s.deliverBlockrequest)
|
||||
assert.NoError(t, err, "ListenEndBlock")
|
||||
|
||||
err = abciListener.ListenStateChanges(s.loggerCtx, s.changeSet)
|
||||
assert.NoError(t, err, "ListenCommit")
|
||||
|
||||
s.updateHeight(int64(i + 1))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *PluginTestSuite) updateHeight(n int64) {
|
||||
s.loggerCtx = NewMockContext(n, s.loggerCtx.Logger(), s.loggerCtx.StreamingManager())
|
||||
}
|
||||
|
||||
var (
|
||||
_ context.Context = MockContext{}
|
||||
_ Context = MockContext{}
|
||||
)
|
||||
|
||||
type MockContext struct {
|
||||
baseCtx context.Context
|
||||
height int64
|
||||
logger log.Logger
|
||||
streamingManager Manager
|
||||
}
|
||||
|
||||
func (m MockContext) BlockHeight() int64 { return m.height }
|
||||
func (m MockContext) Logger() log.Logger { return m.logger }
|
||||
func (m MockContext) StreamingManager() Manager { return m.streamingManager }
|
||||
|
||||
func NewMockContext(height int64, logger log.Logger, sm Manager) MockContext {
|
||||
return MockContext{
|
||||
baseCtx: context.Background(),
|
||||
height: height,
|
||||
logger: logger,
|
||||
streamingManager: sm,
|
||||
}
|
||||
}
|
||||
|
||||
func (m MockContext) Deadline() (deadline time.Time, ok bool) {
|
||||
return m.baseCtx.Deadline()
|
||||
}
|
||||
|
||||
func (m MockContext) Done() <-chan struct{} {
|
||||
return m.baseCtx.Done()
|
||||
}
|
||||
|
||||
func (m MockContext) Err() error {
|
||||
return m.baseCtx.Err()
|
||||
}
|
||||
|
||||
func (m MockContext) Value(key any) any {
|
||||
return m.baseCtx.Value(key)
|
||||
}
|
||||
11
server/v2/streaming/types.go
Normal file
11
server/v2/streaming/types.go
Normal file
@ -0,0 +1,11 @@
|
||||
package streaming
|
||||
|
||||
// Manager is the struct that maintains a list of ABCIListeners and configuration settings.
|
||||
type Manager struct {
|
||||
// Listeners for hooking into the message processing of the server
|
||||
// and exposing the requests and responses to external consumers
|
||||
Listeners []Listener
|
||||
|
||||
// StopNodeOnErr halts the node when ABCI streaming service listening results in an error.
|
||||
StopNodeOnErr bool
|
||||
}
|
||||
23
server/v2/streaming/utils.go
Normal file
23
server/v2/streaming/utils.go
Normal file
@ -0,0 +1,23 @@
|
||||
package streaming
|
||||
|
||||
import "cosmossdk.io/core/event"
|
||||
|
||||
func IntoStreamingEvents(events []event.Event) []*Event {
|
||||
streamingEvents := make([]*Event, len(events))
|
||||
|
||||
for _, event := range events {
|
||||
strEvent := &Event{
|
||||
Type: event.Type,
|
||||
}
|
||||
|
||||
for _, eventValue := range event.Attributes {
|
||||
strEvent.Attributes = append(strEvent.Attributes, &EventAttribute{
|
||||
Key: eventValue.Key,
|
||||
Value: eventValue.Value,
|
||||
})
|
||||
}
|
||||
streamingEvents = append(streamingEvents, strEvent)
|
||||
}
|
||||
|
||||
return streamingEvents
|
||||
}
|
||||
17
server/v2/testdata/app.toml
vendored
Normal file
17
server/v2/testdata/app.toml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
[grpc-server]
|
||||
# Enable defines if the gRPC server should be enabled.
|
||||
enable = false
|
||||
# Address defines the gRPC server address to bind to.
|
||||
address = 'localhost:9090'
|
||||
# MaxRecvMsgSize defines the max message size in bytes the server can receive.
|
||||
# The default value is 10MB.
|
||||
max-recv-msg-size = 10485760
|
||||
# MaxSendMsgSize defines the max message size in bytes the server can send.
|
||||
# The default value is math.MaxInt32.
|
||||
max-send-msg-size = 2147483647
|
||||
|
||||
[mock-server-1]
|
||||
# Mock field
|
||||
mock_field = 'default'
|
||||
# Mock field two
|
||||
mock_field_two = 1
|
||||
@ -2,11 +2,11 @@ schema = 3
|
||||
|
||||
[mod]
|
||||
[mod."buf.build/gen/go/cometbft/cometbft/protocolbuffers/go"]
|
||||
version = "v1.34.0-20240312114316-c0d3497e35d6.1"
|
||||
hash = "sha256-Uoo+Gfx/mnzKRoBvmTJC7oUKMrxOT20ibdY6ArGKHLQ="
|
||||
version = "v1.34.1-20240312114316-c0d3497e35d6.1"
|
||||
hash = "sha256-0zQ7x05+UOSAexuSQMFmt8kcY3023CBEOaulmRklnPA="
|
||||
[mod."buf.build/gen/go/cosmos/gogo-proto/protocolbuffers/go"]
|
||||
version = "v1.34.0-20240130113600-88ef6483f90f.1"
|
||||
hash = "sha256-EpsIiMf8GKK56FgjSS3tCZtVCz4apPcJiaqsbhtF3kM="
|
||||
version = "v1.34.1-20240130113600-88ef6483f90f.1"
|
||||
hash = "sha256-xCjFaSPtlyMmDhzBDAdgUStU8TYFOril/goGfGOGAWo="
|
||||
[mod."cloud.google.com/go"]
|
||||
version = "v0.112.2"
|
||||
hash = "sha256-Bk5MD40eefJlyUk96arLU/X1+EHItM7MjRPJtV0CU58="
|
||||
@ -553,11 +553,11 @@ schema = 3
|
||||
version = "v0.0.0-20240415180920-8c6c420018be"
|
||||
hash = "sha256-0Bc66Utj1rydwYngQxTQoTyg1Td2D+nIxukc0zz7XFc="
|
||||
[mod."google.golang.org/genproto/googleapis/rpc"]
|
||||
version = "v0.0.0-20240509183442-62759503f434"
|
||||
hash = "sha256-w2SGX12NeDO33wjxEpHi+OMcpp9SioPtdBuZKUIun+U="
|
||||
version = "v0.0.0-20240513163218-0867130af1f8"
|
||||
hash = "sha256-c+VNAAU3HTu/ZGfpiPnjnzgszcPhU4JOhTTcIdo/PPI="
|
||||
[mod."google.golang.org/grpc"]
|
||||
version = "v1.63.2"
|
||||
hash = "sha256-RmtVjYLam97k7IHTHU7Gn16xNX+GvA9AiLKlQwOiZXU="
|
||||
version = "v1.64.0"
|
||||
hash = "sha256-04Noi8lrzr+4ac2BA7KNXUXN/xZL/A2SsEpC2Hern84="
|
||||
[mod."google.golang.org/protobuf"]
|
||||
version = "v1.34.1"
|
||||
hash = "sha256-qnHqY6KLZiZDbTVTN6uzF4jedxROYlPCYHoiv6XI0sc="
|
||||
|
||||
Loading…
Reference in New Issue
Block a user