gqlgen-contrib/extensions/hive/reporter.go

244 lines
5.2 KiB
Go

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,
}
}