feat: ✨ add hive reporter
This commit is contained in:
parent
798b2a34ba
commit
a865a73835
243
extensions/hive/reporter.go
Normal file
243
extensions/hive/reporter.go
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
package extensions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/99designs/gqlgen/graphql"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/vektah/gqlparser/v2/ast"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Report struct {
|
||||||
|
Size int `json:"size"`
|
||||||
|
Map map[string]OperationMapRecord `json:"map"`
|
||||||
|
Operations []Operation `json:"operations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperationMapRecord struct {
|
||||||
|
Operation string `json:"operation"`
|
||||||
|
OperationName string `json:"operationName,omitempty"`
|
||||||
|
Fields []string `json:"fields"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Operation struct {
|
||||||
|
OperationMapKey string `json:"operationMapKey"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
Execution Execution `json:"execution"`
|
||||||
|
Metadata *Metadata `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Execution struct {
|
||||||
|
Ok bool `json:"ok"`
|
||||||
|
Duration int64 `json:"duration"`
|
||||||
|
ErrorsTotal int64 `json:"errorsTotal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
Client Client `json:"client"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Reporter struct {
|
||||||
|
Endpoint string
|
||||||
|
ApiToken string
|
||||||
|
BatchSize int
|
||||||
|
report *Report
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ interface {
|
||||||
|
graphql.HandlerExtension
|
||||||
|
graphql.ResponseInterceptor
|
||||||
|
} = Reporter{}
|
||||||
|
|
||||||
|
func (Reporter) ExtensionName() string {
|
||||||
|
return "HiveReporter"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Reporter) Validate(graphql.ExecutableSchema) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Reporter) InterceptResponse(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
|
||||||
|
if !graphql.HasOperationContext(ctx) {
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
rc := graphql.GetOperationContext(ctx)
|
||||||
|
|
||||||
|
start := rc.Stats.OperationStart
|
||||||
|
|
||||||
|
resp := next(ctx)
|
||||||
|
|
||||||
|
if rc.Operation == nil {
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := getPreloads(ctx, rc.Operation.SelectionSet)
|
||||||
|
|
||||||
|
var mapKey string
|
||||||
|
|
||||||
|
for key, omr := range r.report.Map {
|
||||||
|
if omr.Operation != rc.RawQuery {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if omr.OperationName != rc.OperationName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(omr.Fields) != len(fields) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
|
||||||
|
for _, f := range omr.Fields {
|
||||||
|
for _, _f := range fields {
|
||||||
|
if f == _f {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mapKey = key
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if mapKey == "" {
|
||||||
|
id := uuid.New()
|
||||||
|
|
||||||
|
opr := OperationMapRecord{
|
||||||
|
Operation: rc.RawQuery,
|
||||||
|
OperationName: rc.OperationName,
|
||||||
|
Fields: fields,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.report.Map[id.String()] = opr
|
||||||
|
mapKey = id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
end := graphql.Now()
|
||||||
|
duration := end.Sub(start)
|
||||||
|
|
||||||
|
var (
|
||||||
|
clientName = rc.Headers.Get("User-Agent")
|
||||||
|
clientVersion string
|
||||||
|
)
|
||||||
|
|
||||||
|
if parts := strings.Split(clientName, "/"); len(parts) > 1 {
|
||||||
|
clientName, clientVersion = parts[0], strings.Split(parts[1], " ")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
op := Operation{
|
||||||
|
OperationMapKey: mapKey,
|
||||||
|
Timestamp: start.UnixMilli(),
|
||||||
|
Execution: Execution{
|
||||||
|
Ok: len(resp.Errors) == 0,
|
||||||
|
Duration: duration.Nanoseconds(),
|
||||||
|
ErrorsTotal: int64(len(resp.Errors)),
|
||||||
|
},
|
||||||
|
Metadata: &Metadata{
|
||||||
|
Client: Client{
|
||||||
|
Name: clientName,
|
||||||
|
Version: clientVersion,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
r.report.Operations[r.report.Size] = op
|
||||||
|
r.report.Size++
|
||||||
|
|
||||||
|
if r.report.Size >= r.BatchSize {
|
||||||
|
r.logger.Info("Sending report to Hive", zap.String("Endpoint", r.Endpoint), zap.Int("Operations", len(r.report.Operations)))
|
||||||
|
jsonData, err := json.Marshal(r.report)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Error marshaling report", zap.Error(err))
|
||||||
|
graphql.AddError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequest(http.MethodPost, r.Endpoint, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Error sending report to Hive", zap.Error(err))
|
||||||
|
graphql.AddError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.ApiToken))
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Error sending report to Hive", zap.Error(err))
|
||||||
|
graphql.AddError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Sent report to Hive", zap.Any("statusCode", response.StatusCode))
|
||||||
|
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
*r.report = Report{
|
||||||
|
Map: map[string]OperationMapRecord{},
|
||||||
|
Operations: make([]Operation, r.BatchSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPreloads(ctx context.Context, selectionSet ast.SelectionSet) []string {
|
||||||
|
return getNestedPreloads(
|
||||||
|
graphql.GetOperationContext(ctx),
|
||||||
|
graphql.CollectFields(graphql.GetOperationContext(ctx), selectionSet, nil),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNestedPreloads(ctx *graphql.OperationContext, fields []graphql.CollectedField, prefix string) (preloads []string) {
|
||||||
|
for _, column := range fields {
|
||||||
|
prefixColumn := getPreloadString(prefix, column.Name)
|
||||||
|
preloads = append(preloads, prefixColumn)
|
||||||
|
preloads = append(preloads, getNestedPreloads(ctx, graphql.CollectFields(ctx, column.Selections, nil), prefixColumn)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPreloadString(prefix, name string) string {
|
||||||
|
if len(prefix) > 0 {
|
||||||
|
return prefix + "." + name
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReporter(endpoint, apiToken string, batchSize int, logger *zap.Logger) *Reporter {
|
||||||
|
return &Reporter{
|
||||||
|
Endpoint: endpoint,
|
||||||
|
ApiToken: apiToken,
|
||||||
|
BatchSize: batchSize,
|
||||||
|
report: &Report{
|
||||||
|
Map: map[string]OperationMapRecord{},
|
||||||
|
Operations: make([]Operation, batchSize),
|
||||||
|
},
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
6
go.mod
6
go.mod
@ -5,12 +5,14 @@ go 1.21.5
|
|||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.17.49
|
github.com/99designs/gqlgen v0.17.49
|
||||||
github.com/getsentry/sentry-go v0.28.1
|
github.com/getsentry/sentry-go v0.28.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.16
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/sosodev/duration v1.3.1 // indirect
|
github.com/sosodev/duration v1.3.1 // indirect
|
||||||
github.com/vektah/gqlparser/v2 v2.5.16 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/sys v0.21.0 // indirect
|
golang.org/x/sys v0.21.0 // indirect
|
||||||
golang.org/x/text v0.16.0 // indirect
|
golang.org/x/text v0.16.0 // indirect
|
||||||
)
|
)
|
||||||
|
6
go.sum
6
go.sum
@ -26,6 +26,12 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
|||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8=
|
github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww=
|
github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
|
Loading…
Reference in New Issue
Block a user