Compare commits
4 Commits
0d1a410e06
...
631ee5903f
Author | SHA1 | Date | |
---|---|---|---|
631ee5903f | |||
a865a73835 | |||
798b2a34ba | |||
200e9eb4f5 |
39
.drone.yml
Normal file
39
.drone.yml
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
kind: pipeline
|
||||
type: kubernetes
|
||||
name: bump version and create release
|
||||
steps:
|
||||
- commands:
|
||||
- pip install -U Commitizen
|
||||
- git config --global user.email "drone@dikurium.ch"
|
||||
- git config --global user.name "Drone"
|
||||
- git config --global url."https://$GIT_USERNAME:$GIT_PASSWORD@gitea.dikurium.ch/".insteadOf
|
||||
"https://gitea.dikurium.ch/"
|
||||
- git config --global --add --bool push.autoSetupRemote true
|
||||
- git fetch --tags
|
||||
- cz bump --yes
|
||||
- git push --follow-tags
|
||||
- cz changelog $(cz version -p) > CZ_CURRENT_CHANGELOG.md
|
||||
environment:
|
||||
GIT_PASSWORD:
|
||||
from_secret: git_token
|
||||
GIT_USERNAME:
|
||||
from_secret: git_user
|
||||
image: python:3.12
|
||||
name: bump version and generate changelog
|
||||
- image: gitea.dikurium.ch/innopeak/drone-gitea-release
|
||||
name: create release
|
||||
settings:
|
||||
gitea_password:
|
||||
from_secret: git_token
|
||||
gitea_url: gitea.dikurium.ch
|
||||
gitea_username:
|
||||
from_secret: git_user
|
||||
notes_file: CZ_CURRENT_CHANGELOG.md
|
||||
owner: innopeak
|
||||
repo: gqlgen-contrib
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
8
.pre-commit-config.yaml
Normal file
8
.pre-commit-config.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
repos:
|
||||
- hooks:
|
||||
- id: commitizen
|
||||
- id: commitizen-branch
|
||||
stages:
|
||||
- push
|
||||
repo: https://github.com/commitizen-tools/commitizen
|
||||
rev: v3.16.0
|
8
cz.yaml
Normal file
8
cz.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
commitizen:
|
||||
major_version_zero: true
|
||||
name: cz_conventional_commits
|
||||
tag_format: $version
|
||||
update_changelog_on_bump: true
|
||||
version: 0.0.1
|
||||
version_scheme: semver
|
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,
|
||||
}
|
||||
}
|
95
extensions/sentry/tracer.go
Normal file
95
extensions/sentry/tracer.go
Normal file
@ -0,0 +1,95 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
var (
|
||||
sentrySdkIdentifier = "sentry.go.gqlgen"
|
||||
)
|
||||
|
||||
var _ interface {
|
||||
graphql.HandlerExtension
|
||||
graphql.ResponseInterceptor
|
||||
graphql.FieldInterceptor
|
||||
} = Sentry{}
|
||||
|
||||
type Sentry struct{}
|
||||
|
||||
func (Sentry) ExtensionName() string {
|
||||
return "Sentry"
|
||||
}
|
||||
|
||||
func (Sentry) Validate(graphql.ExecutableSchema) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r Sentry) InterceptResponse(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
|
||||
hub := sentry.GetHubFromContext(ctx)
|
||||
if hub == nil {
|
||||
hub = sentry.CurrentHub().Clone()
|
||||
ctx = sentry.SetHubOnContext(ctx, hub)
|
||||
}
|
||||
|
||||
if client := hub.Client(); client != nil {
|
||||
client.SetSDKIdentifier(sentrySdkIdentifier)
|
||||
}
|
||||
|
||||
rc := graphql.GetOperationContext(ctx)
|
||||
|
||||
span := sentry.StartTransaction(
|
||||
ctx,
|
||||
operationName(rc),
|
||||
sentry.WithOpName("gql"),
|
||||
sentry.ContinueFromHeaders(
|
||||
rc.Headers.Get(sentry.SentryTraceHeader),
|
||||
rc.Headers.Get(sentry.SentryBaggageHeader),
|
||||
),
|
||||
)
|
||||
defer span.Finish()
|
||||
|
||||
span.SetData("request.query", rc.RawQuery)
|
||||
|
||||
res := next(span.Context())
|
||||
|
||||
if len(res.Errors) != 0 {
|
||||
sentry.CaptureException(res.Errors)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (t Sentry) InterceptField(ctx context.Context, next graphql.Resolver) (interface{}, error) {
|
||||
fc := graphql.GetFieldContext(ctx)
|
||||
|
||||
span := sentry.StartSpan(ctx, "resolver")
|
||||
defer span.Finish()
|
||||
|
||||
if fc.Field.ObjectDefinition != nil {
|
||||
span.Description = fc.Field.ObjectDefinition.Name + "." + fc.Field.Name
|
||||
span.SetData("resolver.object", fc.Field.ObjectDefinition.Name)
|
||||
}
|
||||
|
||||
span.SetData("resolver.path", fc.Path().String())
|
||||
span.SetData("resolver.field", fc.Field.Name)
|
||||
span.SetData("resolver.alias", fc.Field.Alias)
|
||||
|
||||
return next(span.Context())
|
||||
}
|
||||
|
||||
func operationName(rc *graphql.OperationContext) string {
|
||||
requestName := "nameless-operation"
|
||||
if rc.Doc == nil || len(rc.Doc.Operations) == 0 {
|
||||
return requestName
|
||||
}
|
||||
|
||||
op := rc.Doc.Operations[0]
|
||||
if op.Name != "" {
|
||||
requestName = op.Name
|
||||
}
|
||||
|
||||
return requestName
|
||||
}
|
18
go.mod
Normal file
18
go.mod
Normal file
@ -0,0 +1,18 @@
|
||||
module gitea.dikurium.ch/InnoPeak/gqlgen-contrib
|
||||
|
||||
go 1.21.5
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.49
|
||||
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 (
|
||||
github.com/sosodev/duration v1.3.1 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
)
|
40
go.sum
Normal file
40
go.sum
Normal file
@ -0,0 +1,40 @@
|
||||
github.com/99designs/gqlgen v0.17.49 h1:b3hNGexHd33fBSAd4NDT/c3NCcQzcAVkknhN9ym36YQ=
|
||||
github.com/99designs/gqlgen v0.17.49/go.mod h1:tC8YFVZMed81x7UJ7ORUwXF4Kn6SXuucFqQBhN8+BU0=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k=
|
||||
github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||
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/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8=
|
||||
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/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
Loading…
Reference in New Issue
Block a user